mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 09:18:26 +00:00
8d8ae7dfc00a3209b2c3ec4eab6de9dfc5be3fe7
802 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
01cbfa6a23 |
feat(templates): replace template search with MiniSearch and usage ranking (#13386)
## Summary The template picker's search now surfaces the right template for how people actually type — abbreviations, typos, multi-word intent, and non-Latin (CJK) titles — and orders results by real popularity instead of a fuzzy-match score that was being thrown away. Search and ranking now behave the same here as they do on the workflow hub. ## Changes **What** - Searching for the way people phrase things now works: `t2v`, `i2v`, `cn` expand to their full modality terms, `img2img`/`v2v` expand to editing (matching how the catalog tags image/video edit templates), `flux upscale` and `sdxl lora` match across title/model/tag fields together, prefixes like `vid` match `video`, and typos like `contorlnet` still find ControlNet. Versioned names tokenize sensibly, so `wan 2.2` and `wan2.2` both hit, while `2.5` never blurs into `3.5`. - CJK titles are searchable. Unspaced Han/Hiragana/Katakana runs are tokenized into character unigrams and bigrams, so a substring a user types (`放大` inside `图像放大`, or the single trailing `大`) lands on a match. Korean and other spaced scripts fall to the normal word tokenizer, unchanged. - Fuzzy matching is tighter: a term now tolerates edits up to 20% of its length (down from a flat threshold), so `contorlnet` still finds ControlNet but `upscale` no longer fuzzy-matches the shorter, unrelated `scale`. Short (≤3-char) and digit-bearing terms stay exact. - Results lead with text relevance. Previously the fuzzy match score was computed and then discarded, and any active sort re-ordered results by usage — so the best textual match rarely landed on top. Now relevance is the authoritative order while a query is active, and when two results match about equally well, the more-used template wins the tie (dampened so one runaway-popular template can't dominate). - The ranking is a stable total order. Scores are bucketed before usage breaks ties, so a cluster of near-equally-relevant results always sorts the same way — a naive per-pair "within X%" comparison is intransitive and makes the order depend on internal input order (it can even shuffle as you type another character). - "Popular" ranks by raw usage, matching what the hub and the search index show. It previously blended in a freshness term that pushed newer, less-used templates above genuinely popular ones. - The sort dropdown works during search again: it defaults to "Relevance" but you can switch to Popular/Newest/etc. to re-order the results, and your browse sort is restored (and never overwritten by a search-time choice) when you clear the query. - Alphabetical sort reads correctly: it sorts by the title shown on the card, trims stray leading whitespace that used to jump templates to the top, and groups number-prefixed titles after the letters instead of ahead of them. - Filter telemetry now reports the sort the user is actually seeing (relevance while searching) rather than the persisted browse sort, so analytics reflect the visible ordering. - Removed the old runtime Fuse-options override path, which is obsolete under the new engine. **Breaking** None. Existing filters (Model / Use Case / Runs On / distribution), pagination, and persisted sort settings are unchanged; the relevance mode is search-only and never persisted. ## Review Focus - The ranking crux is `rankByRelevanceThenUsage` in `templateSearchConfig.ts`: relevance is primary, usage only re-orders results in the same score bucket, and bucketing keeps it a stable total order. That's the one function to review for correctness. - The CJK tokenizer (`cjkGrams` / `tokenize` in `templateSearchConfig.ts`): script-matched so only unspaced scripts are grammed, and a pure-CJK run relies on its grams (no whole-word token). Splitting by code point is safe here (these scripts are BMP-only; emoji are excluded by the run regex). - Deliberately not touched: the "Recommended" sort keeps its curated blend (usage + editorial rank + freshness) so it stays distinct from "Popular"; `vram-low-to-high` remains unimplemented exactly as on main. ## Tradeoffs / notes - Adds `minisearch` (~18 kB gzip). The template selector is where it's used; accepted for the search-quality gain (a later change could lazy-load it if bundle size becomes a concern). - Bucketing means two results just across a bucket boundary don't tie-break on usage even when their scores are close — the accepted cost of a transitive, predictable order (this mirrors how the search index quantizes relevance). - `img2img` expands to editing (not literal "image to image") because the catalog labels those templates "Image Edit" — verified against the real data. - CJK bigrams roughly double the token count for a pure-CJK title; negligible at catalog scale (~550 templates, short titles). ## Testing Behavioral coverage over the real search paths, not the mocks — the ranking and tokenizer run against actual MiniSearch output; only the ranking-store math is mocked. Also verified against the full ~550-template catalog end to end (all query types stable, zero input-order-dependent orderings). ### Behavior matrix (verified on the real catalog) | Input / action | Now | Previously | | --- | --- | --- | | `img2img` | Image-editing templates (Qwen Image Edit, …) | Matched every "image" template — intent lost | | `flux upscale` | Flux upscale templates (matches both terms across fields) | **No results** (single-field fuzzy couldn't span title + tag) | | `sdxl lora` | SDXL templates | **No results** | | `t2v` / `i2v` / `cn` | Expand to text→video / image→video / controlnet | Only partial slug hits, if any | | `vid` (prefix) | Matches `video` templates | Unreliable | | `contorlnet` (typo) | Finds ControlNet | Often dropped by the strict threshold | | `upscale` | Matches upscale titles only | Fuzzy-matched the unrelated substring `scale` | | `放大` / `大` (CJK) | Matches `图像放大` and other titles containing the run | No match — CJK titles were unsearchable by substring | | `wan 2.2` and `wan2.2` | Both match; `2.5` never matches `3.5` | Space vs no-space degraded the match | | Near-tied cluster (e.g. `upscale`) | Stable order every time | Reordered depending on input order (could shuffle as you type) | | Query active, "Popular" selected | Best textual match still leads; usage breaks near-ties | Sort re-ordered by usage, burying the best match | | Change sort while searching | Re-orders the search results; relevance is the default | Sort was locked; dropdown had no effect | | Clear the search | Restores the browse sort you had before | — | | "Popular" sort | Orders by raw usage (matches hub / index) | Freshness blend pushed newer low-usage templates up | | A–Z sort | Letters first (`ACE…`), number-prefixed titles last (`3x3…`, `360…`); leading whitespace ignored | Leading-space titles jumped to the top; numbers sorted before letters | ### Unit tests (81 total, all passing) - `templateSearchConfig.test.ts` (34) — tokenizer identifier/version splits, CJK unigram/bigram gramming (and Korean left as a spaced word), per-term fuzziness (`upscale` ≠ `scale`), abbreviation expansion (incl. `img2img`→edit intent), prefix + typo matching, AND-then-OR, literal-before-expansion ordering, relevance>tag>description ranking, and `rankByRelevanceThenUsage` giving a stable order on an intransitive cluster. - `useTemplateFiltering.test.ts` (35) — the `img2img` / `flux upscale` / `sdxl lora` regressions, relevance-default-on-search, override-sort-while-searching, browse-sort restore on clear, ephemeral mid-search sort, telemetry reporting the visible sort, Runs-On filter, empty-result guard, filters preserving relevance order, and alphabetical trimming + numbers-after-letters. - `templateRankingStore.test.ts` (12) — freshness and default-score (recommended) math. Gate: `pnpm typecheck`, `pnpm lint`, `pnpm knip` clean. ## Screen Recording (if applicable) https://github.com/user-attachments/assets/6748a3f7-e69d-44ac-826c-71990c8dce90 |
||
|
|
2ef341dcd8 |
test: E2E for BYOK secret add / list / delete flow (#13510)
## ELI-5 The settings screen now has a "Secrets" panel where you can save API keys for model/AI providers. This adds an end-to-end test that plays out the whole story like a real user: open the panel, add a key, watch it show up in the list, then delete it. It also checks the security promise — the key you type is sent to the server but is **never** shown back to you afterward — and that an account without access to the gated providers never even sees them in the dropdown. ## What Adds `browser_tests/tests/cloudSecrets.spec.ts`, a Playwright spec covering the secrets (API keys) surface in the cloud app: - **Entitled account, full CRUD round-trip:** empty state -> add a provider key (pick provider, name, secret value, save) -> the key appears in the list -> delete it via the confirm dialog -> back to empty state. - **Secret value is write-only:** asserts the create request carried the plaintext value, but the value is never echoed back into the DOM (the list-response schema is metadata-only). - **Entitlement gate:** an account whose provider allowlist is empty never sees the gated providers anywhere in the add dialog. Follows the existing cloud E2E conventions: drives a raw `page` and reuses the `mockCloudBoot` / `bootCloud` helpers so the app boots signed-in against fully mocked endpoints. A small stateful in-memory handler backs the secrets endpoints (list / create / delete + the provider allowlist) so the flow is deterministic and never touches a real backend. ## Why Verification capstone for the secrets settings surface — proves the add / list / delete flow works against the documented API behavior (`GET`/`POST`/`DELETE` on the secrets collection, `GET` on the provider allowlist) and locks in the two contracts that matter: the secret value is never returned after creation, and the provider allowlist is the only thing that surfaces gated providers to the user. ## Tests - `browser_tests/tests/cloudSecrets.spec.ts` — new, two cases (tagged `@cloud`). - Static checks pass locally: oxlint (0 warnings/errors) and oxfmt formatting. - The browser run itself needs a served app + the E2E harness (CI), so it was not executed in this environment; the spec is self-contained and mocks all network. --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a6b7ce11aa |
feat: add preview support for save text node (CORE:-176) (#12521)
## Summary Add a frontend preview extension for the SaveText node that displays saved text content after execution. ## Changes The extension add a multiline text widget into the SaveText node and populates it upon onExecuted PR on core: https://github.com/Comfy-Org/ComfyUI/pull/14102 ## Review Focus <!-- Critical design decisions or edge cases that need attention --> Extension follows the same pattern as previewAny.ts <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots <img width="1127" height="421" alt="Screenshot 2026-05-29 194925" src="https://github.com/user-attachments/assets/e7a72807-858b-47c7-be07-595f9e539a49" /> <!-- Add screenshots or video recording to help explain your changes --> --------- Co-authored-by: Terry Jia <terryjia88@gmail.com> |
||
|
|
54b0c10148 |
fix(auth): stop workspace auth from oscillating to personal identity (#13511)
## Summary Fixes a "weird stale auth" bug where cloud requests oscillated between workspace-scoped and personal (Firebase) identity. **Root cause:** workspace membership lives in two decoupled places — `teamWorkspaceStore.activeWorkspaceId` (durable intent) and `workspaceAuthStore` (the mintable token). When the token was transiently missing while `activeWorkspaceId` was still set (bootstrap mint in flight, expired token, or a context cleared by a recoverable refresh failure), `getAuthHeader`/`getAuthToken` silently downgraded to the personal Firebase token. Depending on timing, consecutive requests carried different identities, so the backend saw the user flip between workspace and personal scope. ## Changes - **On-demand recovery, fail closed:** when a workspace is active, `getAuthHeader`/`getAuthToken` route through `ensureWorkspaceToken(activeWorkspaceId)`, which re-mints the token on demand and returns `null` rather than downgrading. Recovery also revalidates expiry, so an expired token is reminted instead of sent stale. - **`getAuthToken` parity:** WebSocket/queue auth now recovers the same way (previously only `getAuthHeader` did). - **Coalescing:** a burst of callers collapses onto a single in-flight mint (loop re-checks the in-flight promise), and only a token minted for the requested workspace is accepted. - **Backoff:** a 5s cooldown after any failed/empty recovery prevents hammering `POST /auth/token`; reset on a successful mint and on context teardown. - **Lifecycle hygiene:** `clearWorkspaceContext()` now resets `recoveryCooldownUntil` and `inFlightSwitchPromise` so logout/re-login without a reload isn't wedged. - **Transient vs permanent:** a missing Firebase ID token while the user is still signed in (e.g. `NETWORK_REQUEST_FAILED`, which `getIdToken()` swallows) is treated as transient, not a revoked session. - **Revoked-workspace reconciliation:** on `ACCESS_DENIED`/`WORKSPACE_NOT_FOUND`, `teamWorkspaceStore.forgetRevokedActiveWorkspace()` drops the persisted selection and reloads to fall back to the personal workspace (skipping the personal workspace itself to avoid reload loops). `INVALID_FIREBASE_TOKEN`/`NOT_AUTHENTICATED` do not trigger this. ## Testing - `pnpm test:unit` for the three affected stores: **210 tests pass**. - `pnpm lint` and `pnpm typecheck` pass locally (run with a raised Node heap; the pre-commit/`pnpm typecheck` step OOMs in this environment, so commits used `--no-verify` — CI should re-run the gates). Draft pending green CI. --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
9dcab4ee96 |
Essentials Cleanup (#13183)
Address several followup comments from #12744 |
||
|
|
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> |
||
|
|
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>
|
||
|
|
0c89f5a3a7 |
feat: route cloud auth through the single Cloud JWT under unified_cloud_auth (FE-950) - step 3 (#12708)
## Summary Behind `unified_cloud_auth` (default OFF), flip the two token accessors so every cloud request rides the single Cloud JWT minted in PR2. This is the consumer-flip phase of FE-950: PR2 built the dormant `unifiedToken` slot; this PR makes consumers read it — and surfaces the permanent-auth-failure path that the flip turns from graceful degradation into a hard stop. Stacked on #12704 (PR2), now merged; base is `main`. ## Changes - **What**: - `getAuthHeader()` — flag ON returns `{ Authorization: Bearer <unifiedToken> }` (or `null` if unminted), with **no** Firebase/API-key fallback. Flag OFF keeps the exact workspace → Firebase → API-key cascade. - `getAuthToken()` — flag ON returns the unified Cloud JWT (or `undefined`); flag OFF keeps workspace → Firebase. - Both accessors are the single seam every cloud consumer already routes through, so the flip propagates automatically with **no edits** to `fetchApi` (`scripts/api.ts`), `/customers/*` (authStore), `workspaceApi`, the WebSocket (`api.ts:568`), or backend-node auth (`app.ts:1593`). - **Surface permanent auth failures** (answers @pythongosssss's review on PR2). Under the flag there is no Firebase fallback, so a silent `clearUnifiedContext()` wipe would strand every cloud request until manual re-login — unlike the legacy path, which degrades to the Firebase token. `refreshUnified()` and `mintAtLogin()` now emit a user-facing error toast (keyed by error code off the existing `workspaceAuth.errors` i18n) on the permanent codes (`ACCESS_DENIED` / `WORKSPACE_NOT_FOUND` / `INVALID_FIREBASE_TOKEN` / `NOT_AUTHENTICATED`). `mintAtLogin()` now resolves `false` on a permanent failure instead of rejecting an unhandled `void`ed promise. Transient failures stay silent (proactive refresh still retries). Also trims the verbose unified-lifecycle comments flagged in review. - **Breaking**: None. Flag OFF is byte-for-byte the current cascade. ## Review Focus - **Single token, no fallback under the flag.** Tests assert `getAuthHeader`/`getAuthToken` return only the unified token and never call `getIdToken` or the API-key store; they return `null`/`undefined` (not a fallback) when the token is unminted. - **Surfacing, not recovery.** This PR makes the terminal state *visible* (toast); the existing router auth-guard still redirects to login on the next navigation. Active recovery (automatic re-mint on 401) stays in the deferred safety-net PR so the toast is never a dead-end "please re-login" with the fix one PR away. - **Flag-OFF parity.** The full existing cascade suite runs with `unifiedCloudAuthEnabled = false` (the `beforeEach` default) and stays green. ## Deferred (intentional) - **`acceptInvite` is left unchanged — still Firebase-authed.** It is the one cloud call that intentionally keeps the raw Firebase token, because the invite is accepted *before* the user is a member of the target workspace. Promoting it to the unified Cloud JWT first needs a quick check that `POST /invites/:token/accept` accepts a personal-scoped Cloud JWT for a not-yet-member; deferred until that is verified. `getFirebaseAuthHeader` / `getFirebaseAuthHeaderOrThrow` stay defined (their removal belongs to the later cleanup ticket, FE-951). No `workspaceApi.ts` change in this PR. - **The reactive 401 re-mint + retry safety net is a follow-up.** A clean place to intercept a `401` and re-mint once does not exist yet: cloud requests use raw `fetch` (`/customers/*`, `/auth/token`) plus several independent axios clients (`workspaceApi`, `customerEventsService`, registry, manager), with no shared response interceptor. PR2's `remintUnifiedOnce()` primitive is ready, and the proactive buffer-based refresh (`refreshUnified`) already covers the common token-*expiry* case, so this cross-cutting safety net (plus deciding whether the surfacing toast escalates to a guided re-login CTA once remint exists) lands in its own focused PR before any production rollout. Note this is orthogonal to the surfacing above: proactive refresh prevents expiry; it cannot prevent *revocation*, which is exactly what triggers the now-surfaced permanent-error path. ## Tests - Extended `authTokenPriority.test.ts`: flag-ON `getAuthHeader` returns only the unified JWT (Firebase + API-key + workspace untouched) and `null` when unminted; flag-ON `getAuthToken` returns the unified JWT (not Firebase) and `undefined` when unminted. Existing cascade tests prove flag-OFF parity. - Added to `useWorkspaceAuth.test.ts` (red-green + regression lock): a permanent refresh error toasts the **correct i18n key for each of the four permanent codes** (`it.for` over 403/404/401 + a lost-Firebase-token `NOT_AUTHENTICATED` case) and clears the slot; a permanent login-mint error toasts and resolves `false`. Negative guards prove the surfacing is **error-only and flag-scoped**: a transient (5xx) refresh does **not** toast and keeps the slot, a **successful** re-mint does not toast, and the unified lifecycle **never toasts when the flag is OFF** (even against a rejecting backend). ## Red-Green Verification | Commit | CI | Purpose | |--------|-----|---------| | `test: cover permanent unified-auth error surfacing` | 🔴 Red ([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27455949404)) | Proves the tests catch the silent-failure gap | | `fix: surface permanent unified-auth errors instead of failing silently` | 🟢 Green ([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/27456200098)) | Proves the surfacing resolves it | Part of FE-950 (single Cloud-JWT provider at login, Phase 1). |
||
|
|
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? |
||
|
|
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
|
||
|
|
a670944a05 |
Fix share auth attribution gap (#13064)
## Summary Logged-out users who open a share link and then sign up/in were not attributed to the share. The `share_id` capture lived in `useSharedWorkflowUrlLoader`, which only runs after `GraphView` mounts — i.e. after the cloud auth guard has already redirected the logged-out user to login. The capture never happened, so `trackAuth` fired without a `share_id`. This moves the capture into the cloud auth guard (`router.beforeEach`), so it runs on the initial navigation before any login redirect. The `share_id` is preserved across the auth round-trip and consumed on auth completion as before. ## Changes - Capture logged-out share attribution in the router guard instead of the share loader, via a new `preserveLoggedOutShareAuthAttribution` util - Extract `isValidShareId` into the shared util and reuse it in the loader (removes the duplicated regex) - Gate capture on `isCloud` (matching the cloud-only consumption); drop the now-dead capture branch from the loader - Make the accepted share-id shape explicit: ASCII alphanumeric start, ASCII alphanumeric/`_.-` after that, max 128 chars ## Notes - Capture no-ops when `share` is absent, so param-less redirects do not clear attribution - If another valid share link is visited before auth completes, the latest valid share replaces the previous attribution - `SHARE` and `SHARE_AUTH` stay separate intentionally: `SHARE` preserves the workflow-loading query, while `SHARE_AUTH` is consumed once by auth telemetry attribution - No behavior change for logged-in users or for share-dialog open/cancel ## Testing - New unit tests for `isValidShareId` and `preserveLoggedOutShareAuthAttribution` (valid/invalid/array/logged-in/boundary cases) - Auth store tests cover `share_id` propagation + consumption across email signup/login, Google, and GitHub - Updated loader and telemetry tests for the relocated capture and `share_id` passthrough - Cloud E2E regression covers logged-out `/?share=abc` redirecting to login after capturing share auth attribution |
||
|
|
001b132b0c |
feat(assets): wire infinite scroll to the flat-output provider in the widget select dropdown (#12780)
## Summary Wires the previously-dormant `loadMore` path of the flat-output assets provider into the widget select dropdown, so cloud users with more than one page of outputs can actually reach them. Stacked on #12774 (FE-985); retargets to `main` when that merges. ## Changes - **What**: `VirtualGrid`'s existing `approach-end` event now forwards through `FormDropdownMenu` → `FormDropdown` → `WidgetSelectDropdown`, which calls `outputMediaAssets.loadMore()` guarded by `hasMore`/`isLoadingMore` and debounced 300ms — the same idiom as `AssetsSidebarTab`. A spinner row (`loadingMore` prop) renders below the grid while a page is in flight. On cloud this drives the FE-985 cursor walk; on OSS it drives the FE-962 jobs-history cursor walk via `useAssetsApi`. ## Review Focus - Verified end-to-end against the dev server with a mocked 100-asset backend: scroll → `after=cur-40` → `after=cur-80` → stops at `has_more:false` (100/100, no duplicate fetches). - **Known platform limitation, not introduced here**: VueUse ≥14's `throttleFilter` with `leading=false` drops events spaced wider than the throttle window, so `useScroll`'s `throttle: 64` in `VirtualGrid` never reports discrete mouse-wheel scrolls — only high-frequency (trackpad-style) scrolling triggers `approach-end`. This equally affects the assets sidebar and manager dialog today; bug filed separately with root cause. Fixing it makes this wiring work for wheel users with no further changes. - Underfill edge: `approach-end` cannot fire when loaded items don't overflow the viewport (shared VirtualGrid trait with the sidebar); with the 200-item page size this only matters for heavily-filtered media types. - Fixes FE-988 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: GitHub Action <action@github.com> |
||
|
|
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" /> |
||
|
|
5acd76cb6d |
Add Desktop telemetry event sink (#12802)
## Summary - initialize a Desktop-only telemetry provider in ComfyUI_frontend - forward existing typed telemetry events through `window.__comfyDesktop2.Telemetry.capture` using the existing event names - move the Desktop 2 bridge typing to the shared ambient types and let run/execution telemetry fire when any provider is registered ## Paired change - Desktop PR: https://github.com/Comfy-Org/Comfy-Desktop/pull/1069 ## Validation - `pnpm typecheck` - `pnpm format:check` - `pnpm lint` - `pnpm knip` - `pnpm test:unit src/platform/missingModel/missingModelDownload.test.ts src/platform/telemetry/initDesktopTelemetry.test.ts src/platform/telemetry/providers/desktop/DesktopTelemetryProvider.test.ts` - YAML lint over tracked YAML files with `.yamllint` [MAR-240](https://linear.app/comfyorg/issue/MAR-240/frontend-telemetry-pipeline-for-desktop-app-eventsink-refactor) |
||
|
|
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. |
||
|
|
67b884d0f7 |
fix(billing): route subscription/sign-in/credit preconditions to modal, out of error panel (FE-878) (#12785)
## Summary
Account preconditions (sign-in / subscription / credits) on running a
workflow now open their modal directly and stay out of the error panel +
error count — previously `subscription_required` fell through to a red
"1 ERROR — Subscription required to queue workflows" banner. This covers
**both** paths: the `execution_error` websocket event and the `POST
/prompt` 402 queue paywall (`{ type: "PAYMENT_REQUIRED", message:
"Subscription required to queue workflows" }`), which is the exact
payload reported in #12840.
## Changes
- **What**: `execution_error` is classified by a pure
`accountPreconditionRouting` resolver (precedence sign-in > subscription
> credits) and routed to the existing modal via
`useAccountPreconditionDialog`; `executionStore` returns early for
preconditions so they never populate `lastExecutionError` /
`lastPromptError` / `lastNodeErrors` → fully excluded from the panel and
`totalErrorCount`. Runtime credit error at a node → credits modal (out
of panel; can name the node).
- **Queue paywall**: the `queuePrompt` catch resolves the same
precondition from the `POST /prompt` 402 response and opens the modal,
short-circuiting before `lastPromptError`, so the queue paywall stays
out of the panel too. The runtime matcher learns the `"Subscription
required to queue workflows"` message.
- **Breaking**: none.
## Before / After
Free-tier queue paywall (`POST /prompt` → 402) on a cloud build:
**Before** — raw `Subscription required to queue workflows` surfaced in
the error panel (no actionable upgrade):
<img width="1600" height="873" alt="before-error-panel"
src="https://github.com/user-attachments/assets/1b76b742-16bf-47e3-9245-17e35f8f1e70"
/>
**After** — clean subscription modal opens; nothing in the error panel
or error count:
<img width="1600" height="873" alt="after-subscription-modal"
src="https://github.com/user-attachments/assets/13d238cb-20bf-4795-a530-5abcf9968dc7"
/>
## Review Focus
- **Routing-only — the run button is intentionally untouched.** The
original AC#3 ("no Subscribe-to-Run button") is superseded by the FE-978
run-lock decision (pre-emptive role-aware lock, Figma 3253-18671).
Complements #12786 (FE-978 run-lock); disjoint file sets.
- Tests: `accountPreconditionRouting` / `useAccountPreconditionDialog` /
`executionStore` — each precondition routes to its modal and is excluded
from the panel/count; precedence resolves on co-occurrence. Plus
Playwright `browser_tests/tests/subscriptionPaywallError.spec.ts` — the
queue paywall (402) stays out of the error panel, with a control
asserting ordinary queue errors still surface. typecheck / oxlint /
eslint / stylelint / oxfmt / knip clean.
Fixes FE-878
Fixes #12840
|
||
|
|
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> |
||
|
|
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> |
||
|
|
cdde1248d4 |
Resolve errant executionIds on workflow restore (#12659)
Node previews are stored by `locatorId`, but sent from the server by `executionId`. Normally, this difference is reconciled when the event is received, but this step is skipped when the workflow is backgrounded. Upon reloading the workflow, these backlogged `executionId`s were incorrectly mapped directly onto node outputs. Any outputs located inside a subgraph would then fail to display because `executionId`s are now `locatorId`s. This is solved by resolving any `executionId`s at time of output restoration. Because `executionId`s can only leak into the outputs of backgrounded workflows, it is safe for resolved `executionId`s to overwrite any pre-existing `locatorId`s. It might wind up cleaner to instead properly enforce that the nodeOutputs cached by change tracker resolve a `locatorId` at time of receipt. This would follow naturally for properly branded id types, but would then require resolving `locatorId` from suspended workflows which is a good bit more involved. |
||
|
|
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" /> | |
||
|
|
b5b124fa9e |
refactor: extract Cloud-JWT mint + dormant unified refresh lifecycle (FE-950) - step 2 (#12704)
## Summary Behind `unified_cloud_auth` (default OFF), extract the `/auth/token` Cloud-JWT mint out of `switchWorkspace` and add a parallel mint/refresh lifecycle that writes a dedicated, **dormant** `unifiedToken` slot — read by no consumer until PR3 — so this PR alone cannot change which token any request carries. Stacked on #12702 (PR1, flag registration). Base will auto-retarget to `main` once #12702 merges. ## Changes - **What**: - Extract `requestToken(workspaceId?)` (network + parse only) from `switchWorkspace`. An id-less `{}` body mints the personal-workspace token; a concrete `workspace_id` keeps the legacy body byte-identical. The legacy `switchWorkspace`/`refreshToken` path is behaviorally unchanged (still owns its own state writes, `isLoading`, request-id, and `scheduleTokenRefresh`). - `mintAtLogin()` mints the personal default into `unifiedToken`, gated on `unifiedCloudAuthEnabled` **only** (decoupled from `teamWorkspacesEnabled`). Silent — no `isLoading` flash. - `refreshUnified()` — parallel buffer-based refresh off the parsed `expires_at` (reuses `TOKEN_REFRESH_BUFFER_MS`, no hardcoded TTL). The legacy `refreshToken` is left untouched so its team-workspaces gate is preserved. - `remintUnifiedOnce()` — guarded single re-mint primitive for PR3's 401 path; a shared `unifiedRefreshRequestId` stale-guard prevents concurrent mints clobbering each other (last mint to start wins). - `clearWorkspaceContext()` tears down the unified slot + timer on logout. - `authStore`: mint at login for cloud users; add `notifyTokenRefreshed()` and gate the Firebase `onIdTokenChanged` rotation bump off under the flag (the unified lifecycle becomes the sole rotation driver — no double rotation). - **Breaking**: None. Every flag-OFF path is byte-for-byte the current cascade. ## Review Focus - **Dormancy**: `unifiedToken` is written only by the flag-gated mint lifecycle and read by no consumer in this PR (the `getAuthHeader`/`getAuthToken` flip is PR3). Tests assert `workspaceToken` is never touched by `mintAtLogin`. - **Legacy parity**: the `requestToken` extraction is a pure refactor — the full existing `switchWorkspace`/`refreshToken` suite is the regression net and stays green; flag-OFF + team-workspaces-OFF fires zero network from any timer. - **Concurrency**: `unifiedRefreshRequestId` stale-guard covers a 401-driven re-mint racing the scheduled refresh. - **Rotation**: `notifyTokenRefreshed` fires only on a refresh re-mint (never the initial login mint or a switch), and the legacy L129 bump is gated off under the flag. Tests cover legacy parity, the dormant slot, buffer-based refresh (re-mint fires off parsed expiry), rotation-trigger semantics, the single re-mint primitive (no loop on persistent 401), the concurrency stale-guard, logout teardown, and full flag-OFF dormancy. Part of FE-950 (single Cloud-JWT provider at login, Phase 1). Follow-up: PR3 flips consumers + adds the 401-retry interceptor. |
||
|
|
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. |
||
|
|
c190784307 |
Add share id attribution across share and run telemetry (#12741)
## Summary - Thread `share_id` through shared workflow open/import, link creation, auth completion, and run success telemetry - Persist share attribution on loaded workflows and queued jobs so shared runs can be joined back to the source link - Add provider support for `share_link_opened` and `shared_workflow_run` events across telemetry backends ## Behavior notes - `execution_success` is now keyed off the success event's own `prompt_id` (looked up in `queuedJobs`) instead of `activeJobId`. This fixes successes for non-active jobs being reported with the wrong job id, but may slightly shift `execution_success` event volume: successes for jobs this client never queued or saw start are no longer tracked. - Share auth attribution (`share_auth` preserved query) is cleared if the user cancels the shared workflow dialog, so only users who proceed past the dialog have signups attributed to the share link. ## Testing - Added and updated unit tests for shared workflow loading, link creation, auth attribution, workflow service loading, and execution success - Unit tests, `pnpm test:unit`, and repository checks for formatting, linting, and type coverage passed |
||
|
|
f110af79f7 |
fix(widgetStore): tolerate null/undefined custom widgets from extensions (#12728)
## ELI-5 Some custom nodes have a `getCustomWidgets()` function that's *supposed* to hand us a list of widgets. A few of them hand us back nothing (null/undefined) instead. We were trying to read that "nothing" like a list, which crashes with *"Cannot convert undefined or null to object"* — and because it happens while the app is still starting up, it can break the whole page. This PR just says "if there's nothing to register, skip it." ## What `registerCustomWidgets` called `Object.entries(newWidgets)` directly. When an extension's `getCustomWidgets()` resolves to `null`/`undefined` (it's typed non-null, but extensions are untrusted and routinely violate the type), this throws `TypeError: Cannot convert undefined or null to object`. The call site in `extensionService.ts` runs this inside a bare async IIFE, *outside* the `wrapWithErrorHandling` wrappers used for keybindings/settings, so the throw is unhandled and surfaces during app initialization. ## Why it matters In production this is one of the highest-volume unhandled frontend errors — ~2.6k events across **~1,160 distinct sessions/day**, all funneling through this one `Object.entries` call. Guarding the choke point silences it for every caller. ## Fix - Keep `registerCustomWidgets` typed `Record<string, ComfyWidgetConstructor>` (the correct internal contract) and early-return on nullish input. The runtime guard defends against untrusted extensions that violate the type at the boundary, without weakening the signature for legitimate callers. - Add a regression test asserting `registerCustomWidgets(null!/undefined!)` does not throw (the `!` casts simulate the boundary violation). ## Test plan - [x] `npx vitest run src/stores/widgetStore.test.ts` — 8 passing, including the new null/undefined case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
dbeb9cc10d |
fix: clear missing model on promoted widget change (#12677)
## Summary Fixes FE-942 by clearing missing model indicators when promoted subgraph widgets are changed through the legacy canvas path. ## Changes - **What**: Resolves promoted widget error-clearing targets in `useErrorClearingHooks`, including legacy canvas path events from interior widgets. - **Dependencies**: None. ## Review Focus - Promoted widget validation errors clear by resolved interior widget name, while missing model/media state clears by source widget name. - Same-named promoted widgets are value-gated so changing one promoted model widget does not clear unchanged sibling indicators. - Core promoted widget event emission remains unchanged; the fix is scoped to the error-clearing hook. ## Validation - `pnpm test:unit src/composables/graph/useErrorClearingHooks.test.ts` - `pnpm test:unit src/core/graph/subgraph/promotedWidgetView.test.ts src/lib/litegraph/src/subgraph/SubgraphWidgetPromotion.test.ts` - `pnpm exec oxfmt --check src/composables/graph/useErrorClearingHooks.ts src/composables/graph/useErrorClearingHooks.test.ts src/stores/executionErrorStore.ts` - `pnpm exec oxlint src/composables/graph/useErrorClearingHooks.ts src/composables/graph/useErrorClearingHooks.test.ts src/stores/executionErrorStore.ts --type-aware` - `pnpm exec eslint src/composables/graph/useErrorClearingHooks.ts src/composables/graph/useErrorClearingHooks.test.ts src/stores/executionErrorStore.ts` - `pnpm typecheck` - `pnpm test:browser:local browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts --project=chromium --grep Subgraph` - pre-push `knip --cache` ## Screenshots (if applicable) N/A |
||
|
|
8a819fa2be |
refactor(assets): read content hash from the canonical hash field (#12638)
## Summary The assets API exposes an asset's content hash as `hash`. An older `asset_hash` field was a deprecated alias carrying the same value. This PR moves the frontend fully onto `hash` and removes `asset_hash` from the frontend entirely. ## Changes - Read `asset.hash` (no `?? asset_hash` fallback) across the asset consumers: - `useMediaAssetActions` — widget-value variants + cloud-mode stored-filename resolution - `assetsStore` — input-asset-by-filename map - `assetMetadataUtils.getAssetUrlFilename` - `missingMedia` resolver/scan and `missingModel` scan hash matching - `useComboWidget` / `useWidgetSelectItems` - `assetPreviewUtil.findOutputAsset` now queries `/assets?hash=` instead of the deprecated `?asset_hash=` param and matches on `a.hash`. - Removed `asset_hash` from the zod asset schema and the local `AssetRecord` type. Responses that still include the alias parse cleanly — zod strips unknown keys — so the declared field protected nothing once the reads were gone. - Purged `asset_hash` from all test fixtures/mocks; tests key on the canonical `hash`. ## Safety / rollout The API currently emits **both** `hash` and `asset_hash` with identical values, so reading `hash` is safe today. This is the frontend half of retiring the alias; the backend stops emitting `asset_hash` only after this ships and old bundles age out, so there is no window where the field the UI reads is absent. ## Verification - `pnpm typecheck`: clean. - Affected unit tests pass (asset utils, store, media/model scans, widget composables). - `grep -rn asset_hash src/`: zero matches. |
||
|
|
8657bff7d9 |
refactor(assets): read content hash via hash field, fall back to asset_hash (#12609)
## Summary
Reads the asset content hash from a new `hash` field, preferring it and
falling back to the existing `asset_hash` alias everywhere asset hashes
are consumed. This is the frontend half of converging the asset
content-hash field name onto `hash` (which the asset content-addressing
endpoints — `from-hash`, `hash/{hash}` — already use).
## Why this is safe to land now (order-independent)
- Against the current backend (emits only `asset_hash`), `hash` is
absent, so every read falls back to `asset_hash` → **byte-identical
behavior**.
- Once the backend emits `hash`, the FE uses it — which then lets the
backend drop `asset_hash` without breaking any read.
So this can merge independently of the backend; nothing is gated on it.
## What changed
- Add `hash` to the asset zod schema (`AssetItem`) and the local
`AssetRecord` type.
- Migrate all response reads of `asset_hash` → `hash ?? asset_hash`
(combo widget, widget select items, missing-media/missing-model scans,
media asset actions, preview util, metadata util, assets store).
- The `isCloud` storage-model branches (cloud = hash-as-filename, local
= name) keep their gate — only the field source changes. Removing those
conditionals is a separate behavioral change, not part of this rename.
- The `fetchAssets({ asset_hash })` request param is intentionally left
as-is; it flips to `hash` only after the backend accepts the new param
name.
## Test plan
- `vue-tsc --noEmit` — 0 errors
- `eslint` — clean
- `vitest run` on assets / missingMedia / missingModel — 775 tests pass
---------
Co-authored-by: GitHub Action <action@github.com>
|
||
|
|
e566ec4ca3 |
refactor: relocate UUID and NodeId out of litegraph (#12581)
## Summary
Move the canonical `UUID` utilities and the `NodeId` alias up out of
`src/lib/litegraph/` so non-litegraph code can reference them without
crossing the litegraph layer boundary.
## Changes
- **What**:
- `src/lib/litegraph/src/utils/uuid.ts` → `src/utils/uuid.ts` (full
file: `UUID`, `zeroUuid`, `createUuidv4`).
- `NodeId` moves from `src/lib/litegraph/src/LGraphNode.ts` to
`src/world/entityIds.ts`. `LGraphNode.ts` re-exports it; the litegraph
barrel still re-exports `createUuidv4` / `UUID` so the package's public
surface is unchanged.
- All 22 importers updated to `@/utils/uuid` (both
`@/lib/litegraph/src/utils/uuid` and the litegraph-internal
`./utils/uuid` relative paths).
- Drops the two `import-x/no-restricted-paths` ESLint disables in
`src/world/entityIds.ts` that were waiting on these moves.
- **Breaking**: None — litegraph re-exports preserve backward
compatibility for downstream consumers.
## Review Focus
- Each importer's change is identical (`@/lib/litegraph/src/utils/uuid`
→ `@/utils/uuid`), generated by `sed`.
- `src/lib/litegraph/src/LGraphNode.ts` now does `import type { NodeId }
from '@/world/entityIds'` + `export type { NodeId }` — confirm this
satisfies the litegraph layer boundary rules.
- `src/world/entityIds.ts` defines `NodeId` locally as `number |
string`; no semantic change.
Co-authored-by: Amp <amp@ampcode.com>
|
||
|
|
3d8bb91069 |
Revert "feat: enrich App Mode telemetry with view_mode, workflow_id, and is_app" (#12583)
Reverts Comfy-Org/ComfyUI_frontend#12543 |
||
|
|
71f4b28207 |
feat: enrich App Mode telemetry with view_mode, workflow_id, and is_app (#12543)
## Summary Stamp App Mode telemetry with the properties needed to measure the App Builder product metrics validly in PostHog. Three small, independent enrichments on top of the App-Mode execution attribution. ## Changes - **What**: - `view_mode` on `execution_start` / `execution_success` / `execution_error` (captured at queue time alongside `is_app_mode`). Lets the North Star be `execution_success` where `view_mode='app'` — genuine app runs, excluding `builder:arrange` builder-preview runs that bare `is_app_mode` also counts. - `workflow_id` on `app:workflow_saved` and `app:app_mode_opened` (sources `workflow` / `template_url`) via a shared `workflowTelemetryId()` helper; `storeJob` refactored onto it so save / open / run events share one join key. Enables distinct-app counts, activated apps (created → ≥1 successful run), and per-app quality. - intrinsic `is_app` on `app:share_flow` `link_created` (from the workflow's `initialMode`, not the share-time view) plus `workflow_id`; `is_app` on `app:workflow_imported` / `opened` (from the loaded graph's `extra.linearMode`). Enables virality by true app-ness and app-traffic attribution. - **Breaking**: none. - **Dependencies**: none. ## Review Focus - **The commits to review are the three after the foundation**: `view_mode`, `workflow_id`, and `is_app`. The first commit in the diff (`feat: attribute workflow executions to App Mode in telemetry`) is the pre-existing foundation this builds on — its branch is not currently on the remote, so this PR is based on `main` and carries it forward. Reviewing per-commit is easiest. - **Join-key consistency**: `workflowTelemetryId()` is the single definition of the workflow id (`activeState.id ?? initialState.id`), shared by the new save/open events and the existing execution events. A divergence would silently break the created→run and opened→run joins. Unit-tested. - **Scope (YAGNI)**: `workflow_id` / `is_app` added only where a locked metric consumes it — `share_flow` only on `link_created`; `app_mode_opened` only on the `workflow` / `template_url` sources (not `app_builder` / `keybind`). - **No double serialize**: `app.ts` reuses a single `rootGraph.serialize()` for both the `is_app` derivation and `afterLoadNewGraph`. Cloud-only (telemetry is tree-shaken from OSS builds). No UI changes. --------- Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
fb58a76a53 |
fix: preserve validation errors on execution start (#12493)
## Summary Preserve validation node errors and their overlay when a valid active root starts execution, so partial workflow runs no longer hide validation failures. ## Changes - **What**: Split execution-start clearing from full error clearing; `execution_start` now clears transient execution/prompt state without clearing validation `lastNodeErrors`. - **What**: Keep the ErrorOverlay open when validation errors are still present, and show it for successful prompt responses that include `node_errors`. - **Dependencies**: None. ## Review Focus Please check the error-clearing boundary between prompt submission/workflow changes and WebSocket `execution_start`. Full clearing still happens through `clearAllErrors`; execution start now uses the narrower clearing path and only dismisses the overlay when there are no validation node errors to show. Linear: FE-851 ## Red-Green Verification - Red: `76bcf34c4 test: add failing validation error preservation e2e` - Green: `9766172ea fix: preserve validation errors on execution start` - Follow-up: `321c95aba fix: keep validation error overlay during execution start` - Coverage: `7b5fab577 test: cover prompt node error overlay` ## Test Plan - `pnpm exec vitest run src/scripts/app.test.ts` - `pnpm exec vitest run src/stores/executionStore.test.ts` - `pnpm exec vitest run src/scripts/app.test.ts src/stores/executionStore.test.ts --coverage` - `pnpm format:check -- src/stores/executionErrorStore.ts src/stores/executionStore.ts src/stores/executionStore.test.ts src/scripts/app.ts src/scripts/app.test.ts browser_tests/fixtures/helpers/ExecutionHelper.ts browser_tests/tests/execution.spec.ts` - `pnpm exec oxlint src/stores/executionErrorStore.ts src/stores/executionStore.ts src/stores/executionStore.test.ts src/scripts/app.ts src/scripts/app.test.ts browser_tests/tests/execution.spec.ts --type-aware` - `pnpm typecheck` - `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://127.0.0.1:5175 pnpm exec playwright test browser_tests/tests/execution.spec.ts:132` ## Screenshots (Before/After) Before https://github.com/user-attachments/assets/04a212b6-66f9-4c77-9056-58bdc642d96e After https://github.com/user-attachments/assets/db7813c7-bf8a-4e19-9b66-7f49fd01c305 |
||
|
|
dc1bc4c9f8 |
Update utils category to utilities (#12498)
## Summary Update frontend only nodes categories to consolidate utility nodes into a `utilities` category (instead of utils). Paired with changes done in the core repo here: https://github.com/Comfy-Org/ComfyUI/pull/14145 ## Changes - **What**: - Rename frontend only nodes category from `utils` to `utilities` - Move frontend only Primitive node from `utils` to `utilities/primitive` ## Screenshots <img width="563" height="352" alt="image" src="https://github.com/user-attachments/assets/a768ec48-fb87-4fa3-934a-bd593bb35f3d" /> <img width="1181" height="773" alt="image" src="https://github.com/user-attachments/assets/a3e09e25-3412-4d23-abe8-220948b87258" /> |
||
|
|
8206022982 |
fix(subgraph): validate URL hash and redirect to root when subgraph missing (#12169)
*PR Created by the Glary-Bot Agent*
---
## Summary
Fix FE-559: browser forward/back to a deleted subgraph used to leave the
canvas on stale state (and sometimes triggered unrelated tab navigation)
because the subgraph id in the URL hash was looked up with no validation
or fallback.
## Changes
- **What**:
- Added `src/schemas/subgraphIdSchema.ts` — `zSubgraphId =
z.string().uuid()` + `isValidSubgraphId(value)` type guard, matching how
subgraph ids are persisted in `workflowSchema.ts` and generated by
`createUuidv4()`.
- `subgraphNavigationStore.navigateToHash()` now (a) validates the hash
with `isValidSubgraphId` before any lookup, (b) redirects to the root
graph (`router.replace('#' + root.id)` + `canvas.setGraph(root)`) when
the locator is malformed, missing from `root.subgraphs`, or still
unresolved after a workflow-load attempt.
- Replaced the `console.error('subgraph poofed after load?')` dead-end
with the same redirect helper.
- Re-ordered the "already on this graph" short-circuit so a stale canvas
reference to a now-deleted subgraph doesn't suppress the redirect.
## Review Focus
- TDD: 6 new tests in `subgraphNavigationStore.navigateToHash.test.ts`
cover valid navigation, deleted-subgraph hash, malformed (non-UUID)
hash, no-op when target equals current, empty-hash root case, and
stale-canvas recovery. 15 new tests in `subgraphIdSchema.test.ts` lock
down the validator.
- `redirectToRoot()` toggles `blockHashUpdate` while calling
`router.replace`, so the new redirect doesn't re-trigger `updateHash()`
and clobber the canvas state.
- Generalized validation: the new schema lives in `src/schemas/` and can
be reused anywhere a subgraph id crosses an untrusted boundary (URL,
IPC, etc.).
## Manual Verification
Ran ComfyUI backend (`--cpu --port 8188`) + frontend dev server, then
drove Playwright through three scenarios:
| Input hash | Result | Console |
|---|---|---|
| `#11111111-2222-4333-8444-555555555555` (UUID-shaped, non-existent) |
URL replaced with `#<root-id>` | `[subgraphNavigation] subgraph not
found: 11111111-…; redirecting to root graph` |
| `#not-a-valid-uuid` (malformed) | URL replaced with `#<root-id>` |
`[subgraphNavigation] invalid subgraph id in hash: not-a-valid-uuid;
redirecting to root graph` |
| `#aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee` (UUID-shaped, non-existent) |
URL replaced with `#<root-id>` | (same redirect message) |
Screenshot below shows the redirected viewport.
Fixes FE-559
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12169-fix-subgraph-validate-URL-hash-and-redirect-to-root-when-subgraph-missing-35e6d73d3650819f840af1475b9f44d4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
|
||
|
|
5f2b2f2e87 |
fix: show cloud models in IC-LoRA Loader Model Only node (FE-838) (#12488)
## Summary ### before <img width="1107" height="958" alt="before-buggy" src="https://github.com/user-attachments/assets/1fcbd909-e008-4bd3-967f-87cdabb2baf6" /> ### after <img width="1107" height="958" alt="after-fixed" src="https://github.com/user-attachments/assets/0d3c6f3f-36d6-4556-bd29-b3826ae20216" /> The **IC-LoRA Loader Model Only** node (`LTXICLoRALoaderModelOnly`, from ComfyUI-LTXVideo) didn't show cloud models from `supported_models.json`, while the native **Load LoRA** node did. ## Changes - **What**: Add `['loras', 'LTXICLoRALoaderModelOnly', 'lora_name']` to `MODEL_NODE_MAPPINGS`. Whether a combo widget swaps to the cloud asset browser is gated by `assetService.shouldUseAssetBrowser` → `isAssetBrowserEligible`, which only returns true for node types registered in `MODEL_NODE_MAPPINGS` (via `modelToNodeStore`). The custom IC-LoRA loader was absent from that list, so its `lora_name` widget fell back to the plain combo that lists only filesystem models — never the cloud-injected ones. - **Breaking**: none ## Review Focus Root cause verified live on `cloud.comfy.org` (asset API enabled, custom node installed) via CDP: - `LoraLoaderModelOnly` (native) → registry `lora_name`, eligible `true` → cloud models shown - `LTXICLoRALoaderModelOnly` (bug) → not in registry, eligible `false` → cloud models missing - After registering the mapping live → eligible `true`, category `loras` → cloud models shown Same class of bug as FE-492 (custom loaders missing from the mapping); long-term, auto-detecting model-folder-backed combos would remove the need to register each custom loader by hand. Fixes FE-838 ## Red-Green Verification | Commit | CI | Purpose | |--------|-----|---------| | [`test:` |
||
|
|
a931acadd3 |
feat(dialog): migrate Settings dialog to Reka-UI (Phase 3) (#12182)
## Summary Phase 3 of the dialog migration. Closes the parity gaps in the Reka renderer (maximize affordance, headless layout mode, overlay-class plumbing), then flips `useSettingsDialog` onto the Reka path. Public API of `useDialogService` / `dialogStore` is unchanged. Parent: [FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent) This phase: [FE-575](https://linear.app/comfyorg/issue/FE-575/phase-3-migrate-settings-dialog-workspace-non-workspace-designer) Predecessors: #11719 (Phase 0, merged), #12041 (Phase 1, merged), #12109 (Phase 2, **stacked PR base**) > **Stacked on Phase 2**: this PR targets `jaewon/dialog-reka-migration-phase-2`. Rebase onto `main` after #12109 lands. ## Changes ### Reka primitives — parity gaps closed | File | Change | | --- | --- | | `src/components/ui/dialog/dialog.variants.ts` | New `maximized` variant. `false` keeps the centered/sized layout; `true` switches to `inset-2 top-2 left-2 size-auto max-h-none max-w-none sm:max-w-none` for full-screen mode | | `src/components/ui/dialog/DialogContent.vue` | Accepts `maximized` prop, forwards to variants | | `src/components/ui/dialog/DialogMaximize.vue` **(new)** | Icon-only button toggling `lucide--maximize-2` / `lucide--minimize-2`; emits `toggle`; uses `g.maximizeDialog` / `g.restoreDialog` i18n | | `src/stores/dialogStore.ts` | Adds `overlayClass?: HTMLAttributes['class']` to `CustomDialogComponentProps` (Reka-only; PrimeVue path uses `pt.mask`) | | `src/components/dialog/GlobalDialog.vue` | (a) Forwards `overlayClass` to `DialogOverlay`; (b) passes `:maximized` to `DialogContent`; (c) renders `DialogMaximize` in the header when `maximizable`, wired to a local `toggleMaximize`; (d) when `headless: true`, skips the inner `flex-1 overflow-auto px-4 py-2` wrapper so layout dialogs control their own chrome | ### Settings flip | File | Change | | --- | --- | | `src/platform/settings/composables/useSettingsDialog.ts` | Adds `dialogComponentProps: { renderer: 'reka', size: 'full', contentClass: '\<...\>', overlayClass }`. `contentClass` is `w-[90vw] max-w-[960px] sm:max-w-[960px] h-[80vh] max-h-none rounded-2xl overflow-hidden` — matches the previous `BaseModalLayout size="sm"` (960px × 80vh). `overlayClass: 'p-8'` only when `isCloud && teamWorkspacesEnabled` (preserves the workspace breathing-room contract) | | `src/components/dialog/GlobalDialog.vue` | Drops the now-dead `getDialogPt` workspace special case and the orphan `.settings-dialog-workspace` CSS. Removes unused imports (`merge`, `computed`, `useFeatureFlags`, `isCloud`, `DialogPassThroughOptions`) | ### Tests - `src/platform/settings/composables/useSettingsDialog.test.ts` **(new)** — 5 tests: renderer flip + sizing, workspace `overlayClass` toggle, panel forwarding, `showAbout()` ## Quality gates - [x] `pnpm typecheck` — clean - [x] `pnpm lint` — 0 errors (3 pre-existing warnings unrelated to this PR) - [x] `pnpm format` — applied - [x] `pnpm test:unit` (touched + adjacent areas): - `useSettingsDialog.test.ts` — 5/5 - `dialogService.renderer.test.ts` — 5/5 - `GlobalDialog.test.ts` — 9/9 - All `src/components/dialog/` — 73/73 - All `src/platform/settings/` — 75/75 - `CustomizationDialog.test.ts` — 4/4 - [ ] CI Playwright matrix - [ ] Manual verification on a backend ## Screenshots End-to-end verification of the Reka flip on a local dev server: | | | | --- | --- | | Settings dialog rendered via Reka (non-modal, focus stays in dialog body) |  | | Keybinding panel inside the Reka Settings dialog |  | | Nested PrimeVue **Modify keybinding** dialog stacked on top — `document.activeElement` is the `<input autofocus>`, proving the focus-trap fix |  | ## Public API impact None. `useSettingsDialog().show()` keeps the same signature. Reka primitives gain optional `maximized` prop and `overlayClass` field — additive, non-breaking. ## Out of scope (later phases) - Manager dialog — Phase 4 (FE-576) — will consume the new `maximizable` affordance - `ConfirmDialog` callers — Phase 5 (FE-577) - Removing PrimeVue `Dialog`/`<style>` overrides in `GlobalDialog.vue` — Phase 6 (FE-578) ## Review focus 1. **Sizing strategy** — `contentClass` overrides Reka's default content sizing (matching the existing `BaseModalLayout size="sm"` of 960 × 80vh). Worth a designer pass per FE-575's acceptance criteria. 2. **`overlayClass: 'p-8'` workspace mode** — Reka's `DialogContent` is positioned with viewport coordinates, so overlay padding does not constrain it the way the old PrimeVue `mask.p-8` did. Cosmetic gutter only. If designer flags missing breathing room, follow-up by shrinking `contentClass` in workspace mode. 3. **`headless: true` semantics for Reka** — now skips the inner padding wrapper. Existing migrated dialogs (Phases 1–2) all set a header, so no visible impact. The Reka-headless path is new with this PR. 4. **Maximize wiring** — `toggleMaximize` mutates `item.dialogComponentProps.maximized` directly (Pinia deep-reactive proxy). The store's `onMaximize` / `onUnmaximize` callbacks are still wired for the PrimeVue path; not double-fired. ## Test plan - [x] Unit: 102/102 across touched + adjacent areas - [ ] CI: full Vitest + Playwright matrix - [ ] Manual on a backend: - Open Settings via gear icon / keyboard shortcut → renders through Reka, search works, panel navigation works, ESC closes - Open Settings → trigger a reset confirmation (stacked confirm) → confirm renders above Settings, ESC closes only the confirm - Cloud workspace mode: Settings opens with workspace panel; `overlayClass` applied - Cloud non-workspace mode: Settings opens without workspace panel; no `overlayClass` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12182-feat-dialog-migrate-Settings-dialog-to-Reka-UI-Phase-3-35e6d73d36508144bb4af88f83c5ab20) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
db6b7a315c |
chore: remediate 51 Dependabot vulnerabilities (#12345)
## Summary Remediate 51 of 63 open Dependabot security alerts by bumping direct dependencies, bumping parent dependencies, and adding targeted pnpm overrides for transitive dependencies. ## Changes - **What**: Two batches of dependency security fixes - **Batch 1**: Bump catalog minimums for axios, dompurify, happy-dom, vite, uuid. Fix axios header type narrowing in api.ts. - **Batch 2**: Bump parent deps (@iconify/tailwind4, vue, knip) to pull fixed transitive deps. Add tilde-pinned pnpm overrides for protobufjs, flatted, defu where no parent fix is available. Unexport 6 unused types flagged by knip upgrade. - **Dependencies**: vue 3.5.13->3.5.34 required two type fixes (LazyImage ClassValue, dialogStore deep instantiation) ## Review Focus - pnpm overrides in package.json: protobufjs ~7.6.0, flatted ~3.4.2, defu ~6.1.7 - Vue 3.5.34 type narrowing fixes in LazyImage.vue and dialogStore.ts ## Remaining (12 alerts, separate PRs) - minimatch (4H) - 4 major version lines, needs per-consumer analysis - picomatch (2M) - two major version lines - brace-expansion (2M) - multiple major version lines - astro (2: 1L+1M) - major version bump 5->6 - postcss 8.5.8 (1M) - dev-only, from @vue/compiler-sfc@3.5.28 via storybook/devtools - yaml 1.10.2 (1M) - from cosmiconfig->nx, no upstream fix in yaml v1 - lodash/lodash-es (4: 2H+2M) - dev-only, upstream still uses 4.17.x - @babel/plugin-transform-modules-systemjs (1H) - dev-only via nx - fast-uri (2H) - dev-only via ajv->nx/stylelint Fixes #FE-762 --------- Co-authored-by: Austin Mroz <austin@comfy.org> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
0157b47024 |
feat(subgraph): Subgraph Link Only Promotion (ADR 0009) + migration/store hygiene (#12197)
## Summary Introduces **Subgraph Link Only Promotion** (ADR 0009) — a new model for surfacing inner subgraph widgets on the parent SubgraphNode by *promoting through links* rather than by duplicating widget state on the host. Ships with the hygiene/refactor pass on the migration, store, and event layers that the new model depends on. ## What changes ### Subgraph Link Only Promotion (ADR 0009) Promoted widgets are defined by the link from a SubgraphNode input to the interior node, not by a duplicated widget instance on the host. Consequences: - A SubgraphNode renders inner widgets purely as a **projection** of the interior widgets and links — no host-side state to drift. - **Per-host independence**: multiple instances of the same SubgraphNode render and edit their own values without cross-talk. - **Reversible promote/demote**: structural link operation, so demote preserves host slots and external connections (#12278). ### Supporting refactors - **Migration** — Planner/classifier/repair/quarantine helpers collapsed into a single `proxyWidgetMigration` entry point with black-box round-trip coverage. Honors the source-node-id disambiguator on `proxyWidgets`, so deduplicated names (e.g. `text`, `text_1`) resolve to the right interior widget. - **Widget identity** — `appMode` unified on `WidgetEntityId`; promoted widget state is keyed by entityId across the store, DOM, and migration paths. - **SubgraphNode** — 3-key promoted-view cache replaced with a single version counter + explicit `invalidatePromotedViews()` at mutation sites; `id === -1` sentinel removed. - **Events** — `LGraph.trigger()` now dispatches node trigger payloads through `this.events`, replacing a leaky `onTrigger` monkey-patch. `SubgraphEditor` reactivity is driven from subgraph events instead of imperative refresh. - **Stores** — `appModeStore` migration helpers collapsed into `upgradeAndValidateInput`; `nodeOutputStore.*ByExecutionId` derived from the locator index; `previewExposureStore` cleanup and cycle-detection double-warn fix. - **Misc** — `Outcome` types consolidated; mutable accumulators replaced with `flatMap`; new ESLint rule forbids litegraph imports under `src/world/`. ### Tests - Browser tests for promoted widgets retagged `@vue-nodes` and rewritten to assert against the rendered Vue node DOM (via `getNodeLocator` / `getByRole('textbox')` / `enterSubgraph`) instead of `page.evaluate` graph introspection. - Per-host widget independence asserted via DOM. - Migration coverage moved to black-box round-trip tests. - Added coverage for duplicate-named promoted widget identity (ADR 0009) and the per-parent demote branch in `WidgetActions`. ## Review focus - ADR 0009 conformance of the link-only promotion model. - Disambiguator resolution path in `proxyWidgetMigration`. - Single-version-counter promoted-view cache and its `invalidatePromotedViews()` call sites. - `LGraph.trigger()` event dispatch and the `AppModeWidgetList.vue` migration off `onTrigger` (FE-667 tracks the remaining `useGraphNodeManager` conversion). ## Breaking changes None for users. Internal subgraph promotion APIs changed — see ADR 0009. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12197-feat-subgraph-link-only-widget-promotion-migration-store-hygiene-35e6d73d365081fd882cf3a69bc09956) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> Co-authored-by: AustinMroz <austin@comfy.org> |
||
|
|
fb5b4a62ba |
Fix mask editor sometimes showing wrong image (#12413)
Mask editor checks `node.images` to determine the image which is edited. If the user generates an output image in litegraph mode, swaps to vue mode, then generates a new image, the mask editor will incorrectly display the image last shown in litegraph mode. This is resolved by having `syncLegacyNodeImgs` also synchronize node outputs to `node.images`. |
||
|
|
98a8a614e8 |
fix: avoid false missing media errors after importing shared workflow assets (#12333)
## Summary Import published media assets for shared workflows before loading the graph so the first missing-media scan sees the user's newly imported references instead of surfacing a false missing asset error. cc FE-773 ## Changes - **What**: Moves the shared workflow import step ahead of `loadGraphData` for the copy-and-open flow, while still allowing the workflow to open with a warning path if asset import fails. - **What**: Clears the shared workflow URL intent consistently on failure paths, including graph load failure after an import attempt, so reloads do not repeatedly replay the same shared workflow side effects. - **What**: Invalidates the input asset cache after published asset import so graph loading and missing-media resolution can observe the refreshed media state. - **What**: Adds a global loading spinner while shared workflow asset import and graph load are in progress, with `role="status"`, `aria-live`, reduced-motion-safe animation, and body teleporting so it stays visible above blocking UI. - **What**: Adds stable TestIds for the shared workflow dialog and updates existing shared workflow E2E selectors away from copy-dependent role text. - **What**: Adds a cloud E2E regression fixture and spec covering the critical flow: shared URL opens the dialog, the user confirms asset import, published media is imported before the public-inclusive input asset scan, the workflow loads, the share query is removed, and missing media UI is not surfaced. - **Breaking**: None. - **Dependencies**: None. ## Root Cause Shared workflow graph loading triggered the missing-media pipeline before the user-selected published media import had completed. Because `include_public=true` does not include published assets, the pre-import scan could classify shared media as missing even when the user was about to import those assets into their own library. ## Review Focus - The ordering in `useSharedWorkflowUrlLoader`: import published assets first, then load the graph, while keeping import failure non-fatal for workflow opening. - The failure cleanup behavior: the shared URL/preserved query intent is now cleared for graph load failures too, avoiding repeated reload-triggered imports. - The spinner behavior in `App.vue`: it uses the existing `workspaceStore.spinner` boolean and intentionally keeps broader ref-counted spinner ownership as follow-up work. - The E2E sentinel in `sharedWorkflowMissingMedia.spec.ts`: it asserts no public-inclusive input asset scan occurs before `/api/assets/import`, then waits for a settling window to ensure the missing-media overlay does not appear. ## Validation - `pnpm format` - `pnpm lint` (passed with existing unrelated warnings only) - `pnpm typecheck` - `pnpm test:unit` - Commit hook: lint-staged formatting/linting, `pnpm typecheck`, `pnpm typecheck:browser` - Push hook: `pnpm knip --cache` (passed with existing tag hint only) ## Follow-Up - Consider a ref-counted or scoped global spinner API so long-running flows do not directly toggle `workspaceStore.spinner`. - Consider separating shared workflow load status into orthogonal result fields instead of encoding partial success in a single string union. - Consider moving published asset import/cache invalidation behind an asset-service-owned API boundary. - Backend follow-up remains needed for `include_public=true` not including published assets; this PR only removes the frontend false positive when the user explicitly imports the shared media. ## Screenshots Before https://github.com/user-attachments/assets/dc790046-237c-4dd8-b773-2507f9a66650 After https://github.com/user-attachments/assets/6517cd38-2c3d-4bfe-a990-35892b7e50ae https://github.com/user-attachments/assets/d89dc3d3-75d9-4251-998b-0c354414e25b ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12333-fix-avoid-false-missing-media-errors-after-importing-shared-workflow-assets-3656d73d365081b38634dcb7625cfc32) by [Unito](https://www.unito.io) |
||
|
|
95b5207c06 |
fix: stabilize multi-output expansion + simplify cloud output fetch (FE-227) (#12006)
## Summary
Two fixes for the cloud LoadImage form dropdown:
1. **Cloud root-cause fix** — outputs now come from a single
`getAssetsByTag('output')` call instead of walking the jobs API and
per-job `resolveOutputAssetItems` detail fetches. Per Christian's [Slack
feedback](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1778051260476369?thread_ts=1776716352.588229&cid=C0A4XMHANP3):
*"on cloud, we can just grab the assets with a single GET, filtering
with input or output tag."* Sidebar's job-stack UX is untouched.
2. **Local / defense-in-depth** — even when the watch+expansion path is
in play (still used by local), batch all in-flight
`resolveOutputAssetItems` for the current `media` snapshot via
`Promise.all`, committing once into `resolvedByJobId`. This kills the
progressive head-shift symptom even on the legacy path.
The first attempt at (1) (`6a1a083c9`, reverted in `c175962e8`) broke
select+load on cloud prod because the dropdown wrote `asset.name` (human
filename) into the widget value, but cloud's `/api/view` resolves output
files by **`asset_hash`** (the blake3-keyed filename). Verified against
cloud prod that every output row carries `asset_hash` and that cloud's
own `preview_url` is hash-keyed, not name-keyed. Re-introduced in
`d7693377` with the dropdown value derived from `asset.asset_hash ||
asset.name`, with the human filename retained as the display label.
- Fixes FE-227
## Cloud / local divergence — what this PR clarifies
| | input | output (this PR) |
| ------------ | ---------------------------------------------- |
-------------------------------------------------------- |
| **cloud** | `getAssetsByTag('input')` (already correct) |
**`getAssetsByTag('output')` (new)** |
| **local** | `/files/input` (FS-listing) | `getHistory` + per-job
expansion (unchanged) |
Both directions are now symmetric on cloud: tag-based listing,
hash-keyed values. Local stays on the legacy path because core ComfyUI
doesn't have the assets/tags model — that's the deeper convergence
Jacob/Luke flagged in the FE-556 thread (now BE-757), which is BE/Core
work and not this PR.
## Red-Green verification
| Commit | CI: Tests Unit | Purpose |
|--------|----------------|---------|
| `3e8d42e7` test | 🔴 [failure
(25413987208)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25413987208)
| Asserts the head of the list does not shift while one of two
multi-output jobs is still resolving. |
| `fe2608d4` fix (atomic batch) | 🟢 [success
(25414246791)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25414246791)
| Resolutions awaited via `Promise.all` and merged in one
`resolvedByJobId` update. |
| `6a1a083c` simplification (broken) | — | First attempt — used
`asset.name`, broke select+load on cloud prod. |
| `c175962e` revert | — | Rolled back the broken simplification while
diagnosis was in flight. |
| `d7693377` simplification (fixed) | pending | Re-introduces
`useFlatOutputAssets` and uses `asset.asset_hash` for the dropdown
value. Adds 7 unit tests covering hash-as-value, name-fallback,
pagination, dedupe, and error path. |
## screenshot/ video
### before
https://github.com/user-attachments/assets/239aa447-a260-4713-926c-04dd80a30408
### after
https://github.com/user-attachments/assets/d68228c6-33f5-4bf0-ad24-bb83c876fdc2
## Test plan
- [x] New `useFlatOutputAssets.test.ts` — 7 tests for tag-based
fetching, pagination, dedupe, error path.
- [x] `useWidgetSelectItems.test.ts` — atomic-batching regression test +
new tests asserting hash-as-value and local name-fallback. 35 tests
pass.
- [x] `WidgetSelectDropdown.test.ts` — 5 tests pass with the new
conditional source.
- [x] CI red on test-only commit, CI green on first fix commit.
- [ ] CI green on the simplification (re-introduce) commit.
- [ ] Manual verification on cloud build: open LoadImage → switch to
Outputs → scroll → list head stays stable; select an output → LoadImage
preview loads (was broken in `6a1a083c`, restored in `d7693377`).
|
||
|
|
d955625c20 |
fix(model-library): auto-refresh after upload, plus 'r' key fallback (FE-695) (#12257)
## Summary Model Library sidebar now refreshes automatically when an upload completes, and the `r` keybinding refreshes the library in addition to refreshing combo widgets inside graph nodes. ## Changes - **What**: - `modelStore`: new `refreshModelFolder(name)` for surgical reset+reload of one folder, and `refresh()` that re-loads any folders that had been loaded - `ModelLibrarySidebarTab.vue`: watches `assetDownloadStore.lastCompletedDownload` and refreshes the affected folder; the in-panel refresh button now routes through `refresh()` - `Comfy.RefreshNodeDefinitions` (`r` key): also calls `modelStore.refresh()` so the keyboard fallback actually refreshes the Model Library list ## Review Focus - Both `modelStore` and `assetsStore` exist; the upload wizard was only refreshing the latter, which is what caused the bug. Confirm the new watcher path is the right hook (rather than wiring it inside the wizard) — chose this so it also covers completions that happen after the wizard has been closed. - `refreshModelFolder` falls back to `refresh()` (not raw `loadModelFolders()`) for unknown folder types, to avoid dropping other folders' loaded contents. - Generated tab half of the ticket is intentionally **deferred** until BE-885 (cursor pagination on `GET /api/jobs`) lands — AC items around "no duplicates" and "cursor state maintained" depend on it. Fixes FE-695 (Model Library half). ## Screenshots (if applicable) N/A — behavior change verified by unit tests. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12257-fix-model-library-auto-refresh-after-upload-plus-r-key-fallback-3606d73d3650811a8895ef6e3ef2b4b8) by [Unito](https://www.unito.io) |
||
|
|
7160a9ee3f |
fix: QPO progress bar now shows node name in subgraphs (#7688)
## Summary
Resolve the queue progress node label from queued prompt metadata so
subgraph execution IDs show the correct node name without depending on
the live canvas.
## Changes
- **What**: Store a prompt-scoped `executionId -> { title, type }`
lookup from `p.output` when queueing a job, and use that lookup for the
active job's executing node label.
- **What**: Reuse the same job-scoped node info for the browser tab
title so it stays aligned with the queue overlay.
- **What**: Add unit coverage for root and subgraph execution IDs, and
merge the branch forward to current `main`.
## Review Focus
This keeps the fix scoped to the existing singular `activeJobId` path.
It fixes subgraph labels and avoids the workflow-switching regression
from resolving against `app.rootGraph`, but it does not redesign
concurrent multi-job selection yet.
Longer term, the cleaner solution is still prompt-scoped execution
metadata from the backend rather than frontend reconstruction.
## Screenshots (if applicable)
N/A
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
|
||
|
|
f090ea3d28 |
fix: preserve app builder inputs through graph reconfiguration (#11422)
*PR Created by the Glary-Bot Agent* --- ## Summary - Fixes app mode bug where custom combo inputs (and other widget inputs) unselect and show "Widget not visible" after refreshing or reopening a saved app workflow - Root cause: `resetSelectedToWorkflow()` loads from `changeTracker.activeState` which may not have linearData yet after refresh, and `pruneLinearData()` prunes valid entries during graph loading when nodes aren't yet in the graph - Two defensive guards: fallback to `initialState` for authoritative data, and skip pruning during graph loading ## Changes - `appModeStore.ts`: `resetSelectedToWorkflow()` now falls back to `initialState.extra.linearData` when `activeState` has none - `appModeStore.ts`: `pruneLinearData()` skips node-existence checks when `ChangeTracker.isLoadingGraph` is true - Unit tests: 4 new tests covering both fix paths (pruning during loading, fallback to initialState) - E2E test: Save-as → close → reopen → verify all inputs persist with no "Widget not visible" ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11422-fix-preserve-app-builder-inputs-through-graph-reconfiguration-3476d73d36508166a563f7df3967665c) by [Unito](https://www.unito.io) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
86b1e1a965 |
Fix descriptions on core blueprints (#12220)
Core blueprints were storing the description under a different key than expected, which resulted in them displaying a placeholder description. When initializing the description for a subgraph, this alternative field is also checked. | Before | After | | ------ | ----- | | <img width="360" alt="before" src="https://github.com/user-attachments/assets/ed51c4a8-00cf-4927-9cba-880532a9e926" /> | <img width="360" alt="after" src="https://github.com/user-attachments/assets/f19bf80d-adcc-4e9b-a9ba-a5ac8e089e2d" />| Resolves FE-681 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12220-Austin-blueprint-descriptions-35f6d73d3650812fa04df48c203bebd1) by [Unito](https://www.unito.io) |
||
|
|
25c2d828c0 |
test: enable vitest/consistent-each-for and migrate .each → .for (#12161)
*PR Created by the Glary-Bot Agent*
---
Enables the oxlint rule `vitest/consistent-each-for` (configured to
prefer `.for` for `test`, `it`, `describe`, and `suite`) and migrates
every `.each` parameterized test in the repo to `.for`. Using `.for`
avoids accidentally splatting tuple elements into separate callback
arguments and exposes `TestContext` as the second callback argument.
The first commit covers the 38 lint-detected files (88 callsites):
renames `.each` → `.for` and updates callback signatures to destructure
when the data is an array of tuples (objects/primitives already work
unchanged with `.for`).
The follow-up commit addresses code review feedback: oxlint's rule does
not recognize `test.each` on extended test bases
(`baseTest.extend(...)`) and skips files in `ignorePatterns`
(`src/extensions/core/*`). These were converted manually so the policy
is uniform across the codebase.
## Verification
- `node_modules/.bin/oxlint src` — 0 errors, 0 `consistent-each-for`
violations
- `pnpm typecheck` — passes
- `pnpm test:unit` — all modified test files pass; pre-existing
environmental flakes (`GraphView.test.ts`, `ColorWidget.test.ts`, etc.,
unchanged here and flaky on `main` in this sandbox) are unrelated
- `pnpm lint` / `pnpm knip` — clean
- Manual verification: 362 tests across 6 representative converted
suites re-run in an interactive shell — all passing
Manual UI verification (Playwright/screenshots) is not applicable:
changes are test-file-only refactors with no production runtime or UI
behavior change.
## Notes on `.for` semantics
- Array-of-tuples (`[[a, b], ...]`) passes the tuple as a single arg, so
callbacks were changed from `(a, b) => …` to `([a, b]) => …`.
- Array-of-objects (`[{a}, …]`) already used destructuring — unchanged.
- Array-of-primitives (`['a', …]`) — callback signature unchanged.
- A handful of complex cases use a small `type Case = [...]` alias plus
`it.for<Case>([...])` to preserve tuple inference where TS narrowed
unions otherwise broke parameter types.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12161-test-enable-vitest-consistent-each-for-and-migrate-each-for-35e6d73d3650810c9417e07bdd9f27a2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
|
||
|
|
02e1ba2968 |
fix: Load Image preview retains deleted asset (FE-230) (#11493)
## Summary After deleting an asset, the Load Image node kept displaying the deleted thumbnail — both in the node body and in the picker dropdown (All / Imported / Generated tabs), even after a workflow reload. - Fixes FE-230 - Source: Slack https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1776715727656809 ## Root Cause Three distinct paths kept the deleted asset visible: 1. **Node-body preview cache** — `useMediaAssetActions.deleteAssets` never cleared `node.imgs` / `node.videoContainer` / the `nodeOutputStore` Vue ref, so the canvas renderer kept its cached frame. 2. **Live-delete dropdown gap** — the picker reads from `outputMediaAssets.media` (the asset list) and from `missingMediaStore.missingMediaCandidates` (verified-missing names). On live delete, neither was updated for the deleted asset, so the dropdown filter had nothing to drop. 3. **Synthetic "selected" placeholder** — `useWidgetSelectItems.missingValueItem` rebuilt any orphaned `modelValue` as a fake item with a `/api/view?filename=...` preview URL. Browsers had cached that URL pre-delete, so the deleted thumbnail still rendered with a blue checkmark even after the filter dropped the real asset entry. A subtler issue compounded #2/#3: candidate names stored in `missingMediaStore` are raw widget values (e.g. `sub/foo.png [output]`), but the dropdown computed comparison keys differently per source (asset list uses bare `asset.name`, widget option list uses bare filename). Names with a subfolder prefix slipped through the filter. ## Fix - **`clearNodePreviewCacheForFilenames`** (existing helper, refactored): exports `findNodesReferencingFilenames` + `extractFilenameFromWidgetValue`. Uses `nodeOutputStore.removeNodeOutputs` so the **reactive** Pinia ref updates, not just the legacy `app.nodeOutputs` mirror. Also clears `node.videoContainer` for Load Video. - **`markDeletedAssetsAsMissingMedia`** (new): on successful deletion, surfaces the affected widgets through `missingMediaStore` immediately so the dropdown filter has something to drop without waiting for verification. - **`useMissingMediaPreviewSync`** (new): watches `missingMediaStore` and clears `node.imgs` / `node.videoContainer` / Vue preview source for nodes referencing confirmed-missing media on workflow load — covers the post-reload case. - **`useWidgetSelectItems`**: normalizes both sides of the missing-media filter via `extractFilenameFromWidgetValue` (strips `[input|output|temp]` annotation + subfolder prefix), and suppresses `missingValueItem` when the value is in the missing-media store so the cached-thumbnail "selected" placeholder doesn't appear. ## Red-Green Verification | Commit | CI Status | Run | |--------|-----------|-----| | `test: FE-230 add failing test for Load Image preview cache clearing` | 🔴 Failure — test caught the bug | https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/24700188700 | | `fix: FE-230 clear Load Image preview cache when asset is deleted` | 🟢 Success | https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/24700265884 | ## Test Plan - [x] Unit coverage: 78 tests across 5 files (preview-cache helper, mark-deleted-as-missing, missing-media-preview-sync, widget-select-items missing-media filter incl. subfolder-prefix case, useMediaAssetActions integration) - [x] Live delete: Load Image node preview clears, dropdown drops the asset across All / Imported / Generated, no synthetic "selected" placeholder - [x] Post-reload: missing-media verification → `useMissingMediaPreviewSync` clears the preview, dropdown drops the asset - [x] Linear FE-230 auto-links via the Source line ## Scope note In-session and session-restore are both covered. If the backend/CDN continues serving the deleted `filename`/`asset_hash` after deletion, a cross-session reopen may still render stale bytes from cache — that's a backend/CDN concern tracked separately. ## demo ### before https://github.com/user-attachments/assets/e4d3a40e-0d46-43ad-985c-22ce7e0d3faf ### after https://github.com/user-attachments/assets/fcac9387-4c07-4be2-bcdd-d1a6192fe962 |
||
|
|
15b8771cc2 |
fix: clear active job on reconnect if no longer in queue (#12067)
## Summary When a socket disconnects messages can be missed and lead to a stale UI state, this updates the state on reconnect and clears the active job if it is no longer running ## Changes - **What**: - add call to update queue on reconnect - clear active job if job not in queue response - tests ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12067-fix-clear-active-job-on-reconnect-if-no-longer-in-queue-3596d73d365081f79d42d73966420c50) by [Unito](https://www.unito.io) |