mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 09:18:26 +00:00
69fd1ddee78966eaae6ab783ae6b5951ecb9b2ad
297 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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> |
||
|
|
cda2929572 |
fix: use thumbnail previews for on-node image grid cells (FE-741) (#12561)
## Summary On-node image grids rendered **full-resolution** `/api/view` images into small grid cells. `nodeOutputStore.buildImageUrls` only appends a thumbnail flag via `app.getPreviewFormatParam()`, which returns `''` whenever `Comfy.PreviewFormat` is empty (the default on a fresh install). So each grid `<img>` received the original URL with no `&preview=`, and the browser downloaded + decoded the full asset into a ~98px cell. Fix: grid cells request a lightweight thumbnail URL via a new `getGridThumbnailUrl` helper — server-resized on cloud (`res`, mirroring `ResultItemImpl.previewUrl`) and re-encoded to a compact format on every backend (`preview=webp;75`). Gallery / full-view URLs stay at full resolution; a URL that already carries a `preview` (user-set `Comfy.PreviewFormat`) and SVGs (the server cannot rasterize them into a preview) are left untouched. Scoped to the Vue `ImagePreview.vue` path; the legacy canvas preview is deprecated. - Linear: [FE-741](https://linear.app/comfyorg/issue/FE-741) ## Before / After (CDP, localhost:5173 → OSS backend, default settings) Reproduced by rendering an on-node grid of 3 input images on a `SaveImage` node. Measured the `/api/view` request for `02_tangled_code.png` (2560×1440) in one ~98px grid cell: | | Grid cell `<img>` src | Transfer | Type | |---|---|---|---| | **Before** | `…&filename=02_tangled_code.png&subfolder=&rand=…` | **3,222,654 B (3.07 MB)** | `image/png` | | **After** | `…&filename=02_tangled_code.png&subfolder=&rand=…&preview=webp;75` | **119,098 B (116 KB)** | `image/webp` | **~27× less bandwidth** per cell (-96.3%). On cloud the URL additionally gets `res=512` for a true server-side downscale. | Before | After | |---|---| | <img width="430" alt="fe-741-before" src="https://github.com/user-attachments/assets/677b19aa-1b89-4a22-a98f-79c122f5b9d6" /> | <img width="430" alt="fe-741-after" src="https://github.com/user-attachments/assets/6b4b30b0-14bb-4f19-8407-01f8e5de9143" /> | On-node rendering is pixel-identical (OSS re-encodes rather than downscales) — no visual regression; the win is the transferred bytes/format shown above. ## Red-Green Verification | Commit | CI | Result | |--------|----|--------| | [`test:` `b1b410b5`](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/26740459200) | 🔴 Red | grid `<img>` still used the full-res URL → test fails | | [`fix:` `2025cbe7`](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/26740850069) | 🟢 Green | grid `<img>` requests a thumbnail → test passes | Follow-up `4fff42ed` hardens SVG handling and adds `getGridThumbnailUrl` unit tests (CI green). ## Test Plan - [x] CI red on test-only commit ([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/26740459200)) - [x] CI green on fix commit ([run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/26740850069)) - [x] CDP before/after on a live on-node grid (network transfer 3.07 MB → 116 KB per cell) - [x] Unit suites green: `ImagePreview` 31/31, `imageUtil` 10/10; gallery/full-view URLs unchanged |
||
|
|
dc46519fa7 |
feat: add app:node_added telemetry event (#12615)
## Summary Adds a new `app:node_added` PostHog event that fires whenever a user adds a node to the canvas, tagged with a `source` discriminator (sidebar_drag, search_modal, paste, programmatic, unknown). Lets us measure how users compose graphs — which we can't infer from any existing event. ## Changes - **What**: Subscribes to `LGraph.onNodeAdded` via `installNodeAddedTelemetry()`. Source is threaded through call sites via a synchronous module-level flag (`withNodeAddSource`) — `addNodeOnGraph` signature is unchanged. - Wired sources: sidebar drag (3 sidebars + canvas drop), search-modal popover, vintage clipboard paste, programmatic adds (job menu, media asset actions). Unrecognized paths fall through as `unknown`. - **Skip on workflow load**: `ChangeTracker.isLoadingGraph` gates out the bulk-add path. `workflow_imported` already covers that population, and 4M imports/month × ~50 nodes would dwarf user-initiated signal. ## Review Focus - Module-level flag is synchronous-only — the source is read inside the synchronous `onNodeAdded` callback that fires during `graph.add()`. Async code (e.g. `createNode`'s `await setTimeout(0)`) doesn't interleave because the wrap is around `graph.add`, not the await. - Unit test covers: fires with current source, defaults to 'unknown', skips during workflow load, preserves existing onNodeAdded subscriber. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.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>
|
||
|
|
671e0cecdf |
chore: upgrade ESLint to v10.4.0 (#12517)
## Summary
Upgrade ESLint from v9 to v10.4.0, bump compatible plugins/configs, and
fix the 32 violations introduced by the new `eslint:recommended` rules.
## Changes
- **What**:
- Catalog bumps in `pnpm-workspace.yaml`:
- `eslint` 9.39.1 → 10.4.0
- `@eslint/js` 9.39.1 → 10.0.1
- `typescript-eslint` 8.49.0 → 8.60.0
- `eslint-plugin-vue` 10.6.2 → 10.9.1
- `eslint-plugin-import-x` 4.16.1 → 4.16.2
- `eslint-plugin-unused-imports` 4.3.0 → 4.4.1
- `@intlify/eslint-plugin-vue-i18n` 4.1.1 → 4.5.0
- Fixed 4 `preserve-caught-error` violations by passing `{ cause }` to
rethrown errors in `useMaskEditorLoader.ts` and `usePainter.ts`.
- Fixed 28 `no-useless-assignment` violations across litegraph
(`LGraph.ts`, `LGraphCanvas.ts`, `LiteGraphGlobal.ts`, `polyfills.ts`)
and seven other files (`colorUtil.ts`, `linkFixer.ts`, `usePaste.ts`,
`audioService.ts`, `versionUtil.ts`, `subscriptionCheckoutTracker.ts`,
`ShiftClick.test.ts`) by removing dead initializers or redundant writes.
- Removed two now-unnecessary `@ts-expect-error` directives on
`importX.flatConfigs.*` (newer plugin ships correct types).
- **Breaking**: None for runtime; consumers using ESLint locally must
use Node ≥20.19/22.13/24 (already required by repo `engines.node:
">=25"`).
## Review Focus
- Audit of the [v10 migration
guide](https://eslint.org/docs/latest/use/migrate-to-10.0.0) found no
other patterns in this codebase to address (no `eslint-env` comments, no
removed `context.*`/`SourceCode` APIs, no `RuleTester` usage, no
affected rule configs).
- `no-useless-assignment` fixes were chosen conservatively: prefer
keeping the variable with a typed declaration over deleting the
statement, except where the assignment was clearly dead after the last
read.
- Per-file diffs in litegraph (especially `LGraphCanvas.ts`) — worth a
glance to confirm intent.
## Verification
- `pnpm exec eslint --version` → v10.4.0
- `pnpm exec eslint src` → 0 errors
- `pnpm typecheck` → clean
- Unit tests for every touched file pass (319/319 in spot checks:
`colorUtil`, `linkFixer`, `audioService`, `usePaste`, `usePainter`,
`versionUtil`, `LGraph`, `litegraph`, all `LGraphCanvas.*`,
`ShiftClick`).
Co-authored-by: Amp <amp@ampcode.com>
|
||
|
|
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> |
||
|
|
de1c1ee1f2 |
fix: add support for parsing python generated json with NaN/infinite (#12217)
## Summary API and other legacy JSON generated by python `json.dumps` can contain `NaN` and `Infinity` which cannot be parsed with JS `JSON.parse`. This adds regex to replace these invalid tokens with `null`. ## Changes - **What**: - add regex replace on bare NaN/infinity tokens after JSON.parse fails - update call sites - tests ## Review Focus - The regex should only rewrite bare NaN/-Infinity/Infinity and not touch string values or other invalid tokens. - A small regex was chosen over JSON5 due to package size (30.3kB Minified, 9kB Minified + Gzipped) or a manual parser due to the unnecessarily complexity vs a single regex replace. - The happy path is run first, the safe parse is only executed if that failed, meaning no overhead the vast majority of the time and no possiblity of corrupting valid workflows due to a bug in the fallback parser - Multiple call sites had to be updated due to pre-existing architecture of the various parsers, an issue for unifying these is logged for future cleanup - New binary fixtures added for validating e2e import using real files ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12217-fix-add-support-for-parsing-python-generated-json-with-NaN-infinite-35f6d73d365081889fc7f4af823f29c1) by [Unito](https://www.unito.io) |
||
|
|
6e9be7b164 |
feat: add Anthropic partner icon (#12216)
*PR Created by the Glary-Bot Agent* --- ## Summary Adds the Anthropic logo to the partner-node icon set so nodes whose category ends in `Anthropic` (e.g. the Claude node added in Comfy-Org/ComfyUI#13867 with `category="api node/text/Anthropic"`) render the correct provider badge in the node library. ## Changes - `packages/design-system/src/icons/anthropic.svg` — new auto-discovered partner icon (Anthropic A glyph, sourced from [lobehub/lobe-icons](https://github.com/lobehub/lobe-icons), uses `fill="currentColor"` for theme adaptation) - `src/utils/categoryUtil.ts` — register Anthropic's brand coral `#D97757` as the badge border color - `packages/design-system/src/css/style.css` — add `anthropic` to the dynamic comfy-icon safelist so Tailwind/Iconify emits CSS for `icon-[comfy--anthropic]` in production builds - `src/utils/categoryUtil.test.ts` — regression tests for `getProviderIcon('Anthropic')` and `getProviderBorderStyle('Anthropic')` ## Verification - `pnpm typecheck` ✓ - `pnpm lint` ✓ (0 errors; 3 pre-existing warnings in unrelated files) - `pnpm format:check` ✓ - `pnpm test:unit -- src/utils/categoryUtil.test.ts` ✓ (13/13) - `pnpm build` ✓ — confirmed `comfy--anthropic` class is emitted into `dist/assets/index-*.css` - Manual visual check via Playwright against `pnpm dev`: injected `<i class="icon-[comfy--anthropic]">` elements at badge size (10px) and 48px alongside the existing OpenAI and BFL icons and confirmed the Anthropic "A" glyph renders correctly in coral. See screenshot. End-to-end visual verification of the live badge in the node library requires Comfy-Org/ComfyUI#13867 to land first (the Claude node is what produces the `Anthropic` category that triggers the icon lookup). Related: Comfy-Org/ComfyUI#13867 ## Screenshots  ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12216-feat-add-Anthropic-partner-icon-35f6d73d36508133a134fcafaf72f4f8) by [Unito](https://www.unito.io) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> |
||
|
|
129bfd9f1b |
fix: fix drop location and zindex of dragged in images (#12194)
## Summary Images dragged into the canvas were placed at the last graph mouse position, which is not updated during the drag event - meaning nodes were created in "random" locations. Additionally, the z-index was not set so newly created nodes can appear under other nodes. ## Changes - **What**: - ensure added nodes are at top level - update graph mouse pos with position from drop event - tests ## Screenshots (if applicable) Before / After https://github.com/user-attachments/assets/34b4652e-a834-4c22-b191-2875a2404ac5 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12194-fix-fix-drop-location-and-zindex-of-dragged-in-images-35e6d73d3650814781edc9f4b4b5b223) 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>
|
||
|
|
0307281ff2 |
fix: highlight missing input slots on Vue nodes (#11950)
## Summary Restores required-input validation highlighting on Vue node input slots. ## Changes - **What**: Passes validation error state from `NodeSlots` to `InputSlot` using node locator IDs, including subgraph and nested subgraph execution IDs. - **What**: Adds unit coverage for root, one-level subgraph, and nested subgraph slot error mapping. - **What**: Adds a Vue Nodes screenshot regression test that asserts the missing required input slot itself receives the error highlight. - **Dependencies**: None. ## Review Focus - Required input errors on Vue-rendered node's slots. - The new Playwright screenshot expectation will need the `New Browser Test Expectation` label for Linux baseline generation. ## Screenshots (if applicable) Before <img width="499" height="324" alt="스크린샷 2026-05-05 오후 3 00 44" src="https://github.com/user-attachments/assets/285fdf91-6d7e-480b-99b9-715705f78914" /> After <img width="482" height="356" alt="스크린샷 2026-05-05 오후 3 01 11" src="https://github.com/user-attachments/assets/51b8db49-eb9c-4155-8aa5-109c0bd7699b" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11950-fix-highlight-missing-input-slots-on-Vue-nodes-3576d73d365081bd85bfd1ea149d45c5) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com> |
||
|
|
341fef46a9 |
refactor: replace unsafe as Error assertions with type guards (#11845)
## Summary Replaces all 7 production `as Error` type assertions with proper `instanceof Error` narrowing or a new `toError()` helper, and adds an ESLint rule to prevent new ones. First slice of #11429 (the `as Error` category — 9 total occurrences, 7 production + 2 in a test file left untouched). ## Changes - **What**: - New `src/utils/errorUtil.ts` exporting `toError(value: unknown): Error` and `getErrorMessage(value: unknown): string | undefined`. `toError` returns the value unchanged if already an `Error`, otherwise wraps it (handles strings, `undefined`, JSON-serializable objects, and circular refs via `String()` fallback). - Refactored 7 production call sites: - `src/services/gateway/registrySearchGateway.ts` — `toError(error)` for `lastError` assignment in fallback loop - `src/platform/cloud/onboarding/auth.ts` (×2) — `toError(error)` for `captureApiError` Sentry calls - `src/renderer/extensions/vueNodes/widgets/composables/audio/useAudioRecorder.ts` — `toError(err)` before forwarding to `options.onError` - `src/extensions/core/load3d/LoaderManager.ts` — replaced `error as Error & { response?: ... }` cast inside `isNotFoundError` with `'response' in error` + nested narrowing - `apps/desktop-ui/src/stores/maintenanceTaskStore.ts` — inline `error instanceof Error ? error.message : String(error)` - `apps/desktop-ui/src/components/maintenance/TaskListPanel.vue` — inline `error instanceof Error ? error.message : undefined` - New ESLint rule (`no-restricted-syntax` block named `comfy/no-unsafe-error-assertion`) banning `TSAsExpression TSTypeReference[typeName.name='Error']` in `src/**` and `apps/*/src/**`, with test files (`*.test.ts`, `*.spec.ts`) excluded. - 12 unit tests for the new helpers in `src/utils/errorUtil.test.ts`. - **Breaking**: none - **Dependencies**: none ## Review Focus - The lint rule is scoped to non-test source files. Test files retain freedom to use `as Error` for fixture construction; only 2 occurrences exist (in `teamWorkspaceStore.test.ts` and `errorDialog.spec.ts`) and they're intentional. - `toError` is duplicated as inline `instanceof` narrowing in `apps/desktop-ui/` rather than imported, since the desktop-ui workspace doesn't share `@/utils/` with the main app and adding a path mapping for one helper felt heavier than two inline guards. - Remaining `as`-on-DOM categories (HTMLElement ×133, HTMLInputElement ×55, HTMLCanvasElement ×36, KeyboardEvent ×7, Element ×3, MouseEvent ×2, Event ×2) are intentionally left for follow-up PRs to keep this one reviewable. Refs #11429 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11845-refactor-replace-unsafe-as-Error-assertions-with-type-guards-3546d73d36508137a015c4f9e8708f23) by [Unito](https://www.unito.io) |
||
|
|
1c541d8577 |
Short circuit asset reuploads, simplify node dnd (#11691)
When an output is dragged from the assets panel onto a node, outputs were being reuploaded. This logic has been simplified to instead reference the existing asset by resolving the annotated path. As part of this change, async drop handlers on nodes are also fixed. Rather than placing obligation of event handling on client code, not respecting async handlers, or completely ignoring return types, the vue drop handler will now simply set `app.dragOverNode` and allow the `document` drop handler to resolve node drag/drop operations without any of the difficulty from propagation. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11691-Short-circuit-asset-reuploads-simplify-node-dnd-34f6d73d36508157af86e6cf09229781) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
98c327b3c6 |
test: add unit tests for colorUtil edge cases (#11671)
## Summary
Extends `colorUtil.test.ts` with boundary tests that the existing suite
did not cover: malformed `parseToRgb` inputs, alpha-hex parsing through
`parseToRgb`, negative hue normalization in `hsbToRgb`, the full
`isTransparent` matrix, HSV-vs-HSB equivalence in `toHexFromFormat`, the
bare-hex prefix path, and a non-primary-color round-trip through
`hexToHsva` / `hsvaToHex`.
## Changes
- **What**: Adds 13 Vitest cases across 5 new `describe` blocks
(`parseToRgb edge cases`, `hsbToRgb normalization`, `isTransparent`,
`toHexFromFormat`, plus a non-primary round-trip in the existing
`hexToHsva / hsvaToHex` block). Uses the existing
`vi.mock('es-toolkit/compat')` memoize stub.
## Review Focus
- The non-primary palette round-trip allows ±1 per RGB channel because
`hsbToRgb` floors while `rgbToHex` rounds; the test asserts the bound
rather than exact equality.
- `parseToRgb` is exercised with alpha-bearing hex (`#f008`,
`#ff000080`); the function returns RGB-only, so the alpha is
intentionally discarded.
- `toHexFromFormat({h, s, v}, 'hsb')` covers the HSV-shaped object path
that wraps `hsbToRgb`.
## Testing
\`\`\`bash
pnpm exec vitest run src/utils/colorUtil.test.ts
pnpm format -- src/utils/colorUtil.test.ts
pnpm lint
pnpm typecheck
pnpm knip
\`\`\`
73 tests pass (60 prior + 13 new).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11671-test-add-unit-tests-for-colorUtil-edge-cases-34f6d73d36508136bac9edfae32815ec)
by [Unito](https://www.unito.io)
|
||
|
|
f404887c96 |
test: add unit tests for linkFixer (#11668)
## Summary Adds unit coverage for `linkFixer` serialized graph repair paths and fixes input-slot repairs being tracked without mutating the serialized workflow data. ## Changes - **What**: Adds 10 Vitest cases for valid links, dry-run reporting, origin/target repair, missing input-slot cleanup, dangling node cleanup, stale link deletion, and silent mode. - **What**: Applies serialized input slot mutations in fix mode and reports `fixed` only after a rerun confirms the graph is clean. - **What**: Adds the design-system package to knip workspace config so the pre-push hook recognizes its exported CSS dependency usage. - **Dependencies**: None. ## Review Focus The workflow validation path calls `fixBadLinks` with serialized workflow JSON, so the new tests stay at that boundary and assert the repaired graph shape directly. ## Test Coverage - CI snapshot for `src/utils/linkFixer.ts`: unit lines 1.8%. - Local targeted coverage: unit lines 83.9%, functions 94.11%, branches 75.15%. ## Testing ```bash pnpm format -- src/utils/linkFixer.ts src/utils/linkFixer.test.ts pnpm test:unit -- src/utils/linkFixer.test.ts pnpm test:unit -- src/utils/linkFixer.test.ts --coverage pnpm typecheck pnpm lint pnpm knip ``` ## screenshoot <img width="1691" height="927" alt="Screenshot 2026-04-28 at 10 09 26 AM" src="https://github.com/user-attachments/assets/ff59888f-bde3-48f5-853a-60df69e84492" /> |
||
|
|
bab2870131 |
test: harden strict parser coverage for long ISO fractions (#11529)
## Summary - run every `parseIsoDateSafe` case with more than 3 fractional-second digits through `withStrictMillisecondParser` - assert the normalized 3-digit timestamp string passed into `Date` for each long-fraction variant - keep the follow-up scoped to test coverage only ## Root cause V8 already accepts and truncates ISO timestamps with more than 3 fractional-second digits, so the existing tests could stay green even if `parseIsoDateSafe` failed to normalize those values before constructing `Date`. Wrapping the long-fraction cases in the strict parser shim makes CI exercise the Safari/WebView-sensitive path the feature is meant to protect. ## Testing - `pnpm exec vitest run src/utils/dateTimeUtil.test.ts` - `pnpm exec eslint src/utils/dateTimeUtil.test.ts` Fixes #11528 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11529-codex-test-harden-strict-parser-coverage-for-long-ISO-fractions-34a6d73d365081119577eb5fb6d4992c) by [Unito](https://www.unito.io) |
||
|
|
ef59f46495 |
refactor: migrate cn imports from @/utils/tailwindUtil shim to @comfyorg/tailwind-utils directly (#11453)
*PR Created by the Glary-Bot Agent* --- ## Summary - Replace all `cn` / `ClassValue` imports from the `@/utils/tailwindUtil` re-export shim with direct imports from `@comfyorg/tailwind-utils` across 198 source files in `src/` and 3 in `apps/desktop-ui/` - Delete both shim files (`src/utils/tailwindUtil.ts` and `apps/desktop-ui/src/utils/tailwindUtil.ts`) - Add explicit `@comfyorg/tailwind-utils` dependency to `apps/desktop-ui/package.json` - Update documentation references in `AGENTS.md`, `docs/guidance/design-standards.md`, and `docs/guidance/vue-components.md` Fixes #11288 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11453-refactor-migrate-cn-imports-from-utils-tailwindUtil-shim-to-comfyorg-tailwind-utils--3486d73d365081ec92cce91fbf88e6e4) by [Unito](https://www.unito.io) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
65b8a5652c |
fix: render dates in Secrets panel for timestamps with >3 fractional-second digits (#11358)
## Summary
Cloud Prod renders "Invalid Date" in Settings → Secrets on strict JS
Date parsers (older Safari, some WebViews) because the backend emits
timestamps with variable fractional-second precision (e.g.
`"2026-04-18T10:04:55.6513Z"` — 4 digits), which falls outside the
3-digit-only ECMA-262 grammar.
## Changes
- **What**:
- Add `parseIsoDateSafe()` in `src/utils/dateTimeUtil.ts` — trims the
fractional portion to millisecond precision before `new Date(...)` and
returns `null` for missing or unparseable input.
- `SecretListItem.vue` uses the helper and hides the Created / Last Used
line when the timestamp is invalid instead of rendering the literal
string "Invalid Date".
- Unit tests for the parser (8) and for the component (4-digit
fractional seconds, garbage input).
## Review Focus
- The backend (Go `time.RFC3339Nano`) strips trailing zeros from
fractional seconds, producing 0–9 digits depending on the value. Modern
V8 parses this leniently; older Safari does not. A durable fix is
server-side — emit exactly 3 fractional digits — and should be filed
separately. This PR is a defensive frontend guard that also protects ~10
other `toLocaleDateString` callsites if they migrate to the helper.
- Regex `(\.\d{3})\d+(?=Z|[+-]\d{2}:?\d{2}|$)` trims only when there are
**more than** 3 digits; shorter fractions and zero-fraction timestamps
are unchanged.
## Screenshots (if applicable)
Reported in Slack:
https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1776443594202969
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11358-fix-render-dates-in-Secrets-panel-for-timestamps-with-3-fractional-second-digits-3466d73d3650813cb855cfbd50b3650b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Terry Jia <terryjia88@gmail.com>
|
||
|
|
983789753e |
refactor: remove @ts-expect-error suppressions in test files (#11337)
## Summary
Part of #11092 — Phase 3: remove @ts-expect-error suppressions from test
files.
This phase targets 22 suppressions across two test files:
- `src/utils/nodeDefUtil.test.ts` (18)
- `src/platform/workflow/validation/schemas/workflowSchema.test.ts` (4)
## Changes
`nodeDefUtil.test.ts`: Each test already constrains the inputs to a
known subtype (`IntInputSpec`, `FloatInputSpec`, `ComboInputSpecV2`), so
casting result to the expected subtype at the declaration site is both
correct and self-documenting. For the one test that uses the base
`InputSpec` type, the options object is extracted with an inline
structural cast.
`workflowSchema.test.ts`: validateComfyWorkflow returns
ComfyWorkflowJSON | null. The tests were accessing .nodes[0].pos without
narrowing, causing "object is possibly null" errors. Fixed with explicit
expect(validatedWorkflow).not.toBeNull() assertions before each property
access, which also improves failure messages — previously a null result
would throw a TypeError rather than a readable assertion failure.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Test-only type-safety refactor with no runtime code changes; primary
risk is minor test assertion behavior changes if a helper unexpectedly
returns `null`.
>
> **Overview**
> Removes `@ts-expect-error` suppressions from two test suites by making
nullability and return-type expectations explicit.
>
> `workflowSchema.test.ts` now asserts `validateComfyWorkflow` results
are non-null before accessing `nodes[0]` fields, and
`nodeDefUtil.test.ts` casts `mergeInputSpec` results to the expected
spec subtype (or extracts typed options) so property assertions compile
cleanly under stricter TS settings.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
|
||
|
|
2f4116fa81 |
test: add unit tests for numberUtil and dateTimeUtil (#11253)
## Summary Adds unit tests for two untested utility modules to improve coverage: - **`numberUtil.ts`** — `clampPercentInt`, `formatPercent0` (clamping, rounding, locale formatting) - **`dateTimeUtil.ts`** — `dateKey`, `isToday`, `isYesterday`, `formatShortMonthDay`, `formatClockTime` 20 new tests total. This PR also serves as an E2E validation of the coverage Slack notification workflow (#10977) — merging should trigger a Slack notification showing the coverage improvement. ## Test Plan - `pnpm test:unit -- src/utils/numberUtil.test.ts src/utils/dateTimeUtil.test.ts` - All 20 tests pass locally ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11253-test-add-unit-tests-for-numberUtil-and-dateTimeUtil-3436d73d365081aab388fd1f1fcac7d7) by [Unito](https://www.unito.io) |
||
|
|
a1e6fb36d2 |
refactor: harden ChangeTracker lifecycle with self-defending API (#10816)
## Summary Harden the `ChangeTracker` lifecycle to eliminate the class of bugs where an inactive workflow's tracker silently captures the wrong graph state. Renames `checkState()` to `captureCanvasState()` with a self-defending assertion, introduces `deactivate()` and `prepareForSave()` lifecycle methods, and closes a latent undo-history corruption bug discovered during code review. ## Background ComfyUI supports multiple workflows open as tabs, but only one canvas (`app.rootGraph`) exists at a time. When the user switches tabs, the old workflow's graph is unloaded and the new one is loaded into this shared canvas. The old `checkState()` method serialized `app.rootGraph` into `activeState` to track changes for undo/redo. It had no awareness of *which* workflow it belonged to -- if called on an inactive tab's tracker, it would capture the active tab's graph data and silently overwrite the inactive workflow's state. This caused permanent data loss (fixed in PR #10745 with caller-side `isActive` guards). The caller-side guards were fragile: every new call site had to remember to add the guard, and forgetting would reintroduce the same silent data corruption. Additionally, `beforeLoadNewGraph` only called `store()` (viewport/outputs) without `checkState()`, meaning canvas state could be stale if a tab switch happened without a preceding mouseup event. ### Before (fragile) ``` saveWorkflow(workflow): if (isActive(workflow)) <-- caller must remember this guard workflow.changeTracker.checkState() <-- name implies "read", actually writes ... beforeLoadNewGraph(): activeWorkflow.changeTracker.store() <-- only saves viewport, NOT graph state ``` ### After (self-defending) ``` saveWorkflow(workflow): workflow.changeTracker.prepareForSave() <-- handles active/inactive internally ... beforeLoadNewGraph(): activeWorkflow.changeTracker.deactivate() <-- captures graph + viewport together ``` ## Changes - Rename `checkState` to `captureCanvasState` with active-tracker assertion - Add `deactivate()` and `prepareForSave()` lifecycle methods - Fix undo-history corruption: `captureCanvasState()` guarded by `_restoringState` - Fix viewport regression during undo: `deactivate()` skips `captureCanvasState()` during undo/redo but always calls `store()` to preserve viewport (regression from PR #10247) - Log inactive tracker warnings unconditionally at warn level (not DEV-only) - Deprecated `checkState()` wrapper for extension compatibility - Rename `checkState` to `captureCanvasState` in `useWidgetSelectActions` composable - Add `appModeStore.ts` to manual call sites documentation - Add `checkState()` deprecation note to architecture docs - Add 16 unit tests covering all guard conditions, lifecycle methods, and undo behavior - Add E2E test: "Undo preserves viewport offset" ## New ChangeTracker Public API | Method | Caller | Purpose | |--------|--------|---------| | `captureCanvasState()` | Event handlers, UI interactions | Snapshots canvas into activeState, pushes undo. Asserts active tracker. | | `deactivate()` | `beforeLoadNewGraph` only | `captureCanvasState()` (skipped during undo/redo) + `store()`. Freezes state for tab switch. | | `prepareForSave()` | Save paths only | Active: `captureCanvasState()`. Inactive: no-op. | | `checkState()` | **Deprecated** -- extensions only | Wrapper that delegates to `captureCanvasState()` with deprecation warning. | | `store()` | Internal to `deactivate()` | Saves viewport, outputs, subgraph navigation. | | `restore()` | `afterLoadNewGraph` | Restores viewport, outputs, subgraph navigation. | | `reset()` | `afterLoadNewGraph`, save | Resets initial state (marks as "clean"). | ## Test plan - [x] Unit tests: 16 tests covering all guard conditions, state capture, undo queue behavior - [x] E2E test: "Undo preserves viewport offset" verifies no viewport drift on undo - [x] E2E test: "Prevents captureCanvasState from corrupting workflow state during tab switch" - [x] Existing E2E: "Closing an inactive tab with save preserves its own content" - [ ] Manual: rapidly switch tabs during undo/redo, verify no viewport drift - [ ] Manual: verify extensions calling `checkState()` see deprecation warning in console |
||
|
|
83ceef8cb3 |
test: add regression test for non-string serverLogs (#8460) (#11268)
## Summary Add a regression test for #8460 (handle non-string `serverLogs` in error report). The fix added `typeof error.serverLogs === 'string' ? ... : JSON.stringify(...)` in `errorReportUtil.ts` so object-shaped logs no longer render as `[object Object]`. There was no existing unit test for `generateErrorReport`, so this regression could silently return. ## Changes - **What**: New unit test file `src/utils/errorReportUtil.test.ts` covering `generateErrorReport`'s `serverLogs` rendering. - String case: verifies plain-string logs still appear verbatim and `[object Object]` is absent. - Object case (regression for #8460): verifies object logs are JSON-stringified instead of coerced to `[object Object]`. - Verified RED→GREEN locally. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11268-test-add-regression-test-for-non-string-serverLogs-8460-3436d73d36508195a32fc559ab7ce5bb) by [Unito](https://www.unito.io) |
||
|
|
693b8383d6 |
fix: missing-asset correctness follow-ups from #10856 (#11233)
Follow-up to #10856. Four correctness issues and their regression tests. ## Bugs fixed ### 1. ErrorOverlay model count reflected node selection `useErrorGroups` exposed `filteredMissingModelGroups` under the public name `missingModelGroups`. `ErrorOverlay.vue` read that alias to compute its model count label, so selecting a node shrank the overlay total. The overlay must always show the whole workflow's errors. Exposed both shapes explicitly: `missingModelGroups` / `missingMediaGroups` (unfiltered totals) and `filteredMissingModelGroups` / `filteredMissingMediaGroups` (selection-scoped). `TabErrors.vue` destructures the filtered variant with an alias. Before https://github.com/user-attachments/assets/eb848c5f-d092-4a4f-b86f-d22bb4408003 After https://github.com/user-attachments/assets/75e67819-c9f2-45ec-9241-74023eca6120 ### 2. Bypass → un-bypass dropped url/hash metadata Realtime `scanNodeModelCandidates` only reads widget values, so un-bypass produced a fresh candidate without the url that `enrichWithEmbeddedMetadata` had previously attached from `graphData.models`. `MissingModelRow`'s download/copy-url buttons disappeared after a bypass/un-bypass cycle. Added `enrichCandidateFromNodeProperties` that copies `url`/`hash`/`directory` from the node's own `properties.models` — which persists across mode toggles — into each scanned candidate. Applied to every call site of the per-node scan. A later fix in the same branch also enforces directory agreement to prevent a same-name / different-directory collision from stamping the wrong metadata. Before https://github.com/user-attachments/assets/39039d83-4d55-41a9-9d01-dec40843741b After https://github.com/user-attachments/assets/047a603b-fb52-4320-886d-dfeed457d833 ### 3. Initial full scan surfaced interior errors of a muted/bypassed subgraph container `scanAllModelCandidates`, `scanAllMediaCandidates`, and the JSON-based missing-node scan only check each node's own mode. Interior nodes whose parent container was bypassed passed the filter. Added `isAncestorPathActive(rootGraph, executionId)` to `graphTraversalUtil` and post-filter the three pipelines in `app.ts` after the live rootGraph is configured. The filter uses the execution-ID path (`"65:63"` → check node 65's mode) so it handles both live-scan-produced and JSON-enrichment-produced candidates. Before https://github.com/user-attachments/assets/3032d46b-81cd-420e-ab8e-f58392267602 After https://github.com/user-attachments/assets/02a01931-951d-4a48-986c-06424044fbf8 ### 4. Bypassed subgraph entry re-surfaced interior errors `useGraphNodeManager` replays `graph.onNodeAdded` for each existing interior node when the Vue node manager initializes on subgraph entry. That chain reached `scanSingleNodeErrors` via `installErrorClearingHooks`' `onNodeAdded` override. Each interior node's own mode was active, so the caller guards passed and the scan re-introduced the error that the initial pipeline had correctly suppressed. Added an ancestor-activity gate at the top of `scanSingleNodeErrors`, the single entry point shared by paste, un-bypass, subgraph entry, and subgraph container activation. A later commit also hardens this guard against detached nodes (null execution ID → skip) and applies the same ancestor check to `isCandidateStillActive` in the realtime verification callback. Before https://github.com/user-attachments/assets/fe44862d-f1d6-41ed-982d-614a7e83d441 After https://github.com/user-attachments/assets/497a76ce-3caa-479f-9024-4cd0f7bd20a4 ## Tests - 6 unit tests for `isAncestorPathActive` (root, active, immediate-bypass, deep-nested mute, unresolvable ancestor, null rootGraph) - 4 unit tests for `enrichCandidateFromNodeProperties` (enrichment, no-overwrite, name mismatch, directory mismatch) - 1 unit test for `scanSingleNodeErrors` ancestor guard (subgraph entry replaying onNodeAdded) - 2 unit tests for `useErrorGroups` dual export + ErrorOverlay contract - 4 E2E tests: - ErrorOverlay model count stays constant when a node is selected (new fixture `missing_models_distinct.json`) - Bypass/un-bypass cycle preserves Copy URL button (uses `missing_models_from_node_properties`) - Loading a workflow with bypassed subgraph suppresses interior missing model error (new fixture `missing_models_in_bypassed_subgraph.json`) - Entering a bypassed subgraph does not resurface interior missing model error (shares the above fixture) `pnpm typecheck`, `pnpm lint`, 206 related unit tests passing. ## Follow-up Several items raised by code review are deferred as pre-existing tech debt or scope-avoided refactors. Tracked via comments on #11215 and #11216. --- Follows up on #10856. |
||
|
|
521019d173 |
fix: exclude muted/bypassed nodes from missing asset detection (#10856)
## Summary Muted and bypassed nodes are excluded from execution but were still triggering missing model/media/node warnings. This PR makes the error system mode-aware: muted/bypassed nodes no longer produce missing asset errors, and all error lifecycle events (mode toggle, deletion, paste, undo, tab switch) are handled consistently. - Fixes Comfy-Org/ComfyUI#13256 ## Behavioral notes - **Tab switch overlay suppression (intentional)**: Switching back to a workflow with missing assets no longer re-shows the error overlay. This reverses the behavior introduced in #10190. The error state is still restored silently in the errors tab — users can access it via the properties panel without being interrupted by the overlay on every tab switch. ## Changes ### 1. Scan filtering - `scanAllModelCandidates`, `scanAllMediaCandidates`, `scanMissingNodes`: skip nodes with `mode === NEVER || BYPASS` - `collectMissingNodes` (serialized data): skip error reporting for muted/bypassed nodes while still calling `sanitizeNodeName` for safe `configure()` - `collectEmbeddedModelsWithSource`: skip muted/bypassed nodes; workflow-level `graphData.models` only create candidates when active nodes exist - `enrichWithEmbeddedMetadata`: filter unmatched workflow-level models when all referencing nodes are inactive ### 2. Realtime mode change handling - `useErrorClearingHooks.ts` chains `graph.onTrigger` to detect `node:property:changed` (mode) - Deactivation (active → muted/bypassed): remove missing model/media/node errors for the node - Activation (muted/bypassed → active): scan the node and add confirmed errors, show overlay - Subgraph container deactivation: remove all interior node errors (execution ID prefix match) - Subgraph container activation: scan all active interior nodes recursively - Subgraph interior mode change: resolve node via `localGraph.getNodeById()` then compute execution ID from root graph ### 3. Node deletion - `graph.onNodeRemoved`: remove missing model/media/node errors for the deleted node - Handle `node.graph === null` at callback time by using `String(node.id)` for root-level nodes ### 4. Node paste/duplicate - `graph.onNodeAdded`: scan via `queueMicrotask` (deferred until after `node.configure()` restores widget values) - Guard: skip during `ChangeTracker.isLoadingGraph` (undo/redo/tab switch handled by pipeline) - Guard: skip muted/bypassed nodes ### 5. Workflow tab switch optimization - `skipAssetScans` option in `loadGraphData`: skip full pipeline on tab switch - Cache missing model/media/node state per workflow via `PendingWarnings` - `beforeLoadNewGraph`: save current store state to outgoing workflow's `pendingWarnings` - `showPendingWarnings`: restore cached errors silently (no overlay), always sync missing nodes store (even when null) - Preserve UI state (`fileSizes`, `urlInputs`) on tab switch by using `setMissingModels([])` instead of `clearMissingModels()` - `MissingModelRow.vue`: fetch file size on mount via `fetchModelMetadata` memory cache ### 6. Undo/redo overlay suppression - `silentAssetErrors` option propagated through pipeline → `surfaceMissingModels`/`surfaceMissingMedia` `{ silent }` option - `showPendingWarnings` `{ silent }` option for missing nodes overlay - `changeTracker.ts`: pass `silentAssetErrors: true` on undo/redo ### 7. Error tab node filtering - Selected node filters missing model/media card contents (not just group visibility) - `isAssetErrorInSelection`: resolve execution ID → graph node for selection matching - Missing nodes intentionally unfiltered (pack-level scope) - `hasMissingMediaSelected` added to `RightSidePanel.vue` error tab visibility - Download All button: show only when 2+ downloadable models exist ### 8. New store functions - `missingModelStore`: `addMissingModels`, `removeMissingModelsByNodeId` - `missingMediaStore`: `addMissingMedia`, `removeMissingMediaByNodeId` - `missingNodesErrorStore`: `removeMissingNodesByNodeId` - `missingModelScan`: `scanNodeModelCandidates` (extracted single-node scan) - `missingMediaScan`: `scanNodeMediaCandidates` (extracted single-node scan) ### 9. Test infrastructure improvements - `data-testid` on `RightSidePanel.vue` tabs (`panel-tab-{value}`) - Error-related TestIds moved from `dialogs` to `errorsTab` namespace in `selectors.ts` - Removed unused `TestIdValue` type - Extracted `cleanupFakeModel` to shared `ErrorsTabHelper.ts` - Renamed `openErrorsTabViaSeeErrors` → `loadWorkflowAndOpenErrorsTab` - Added `aria-label` to pencil edit button and subgraph toggle button ## Test plan ### Unit tests (41 new) - Store functions: `addMissing*`, `removeMissing*ByNodeId` - `executionErrorStore`: `surfaceMissing*` silent option - Scan functions: muted/bypassed filtering, `scanNodeModelCandidates`, `scanNodeMediaCandidates` - `workflowService`: `showPendingWarnings` silent, `beforeLoadNewGraph` caching ### E2E tests (17 new in `errorsTabModeAware.spec.ts`) **Missing nodes** - [x] Deleting a missing node removes its error from the errors tab - [x] Undo after bypass restores error without showing overlay **Missing models** - [x] Loading a workflow with all nodes bypassed shows no errors - [x] Bypassing a node hides its error, un-bypassing restores it - [x] Deleting a node with missing model removes its error - [x] Undo after bypass restores error without showing overlay - [x] Pasting a node with missing model increases referencing node count - [x] Pasting a bypassed node does not add a new error - [x] Selecting a node filters errors tab to only that node **Missing media** - [x] Loading a workflow with all nodes bypassed shows no errors - [x] Bypassing a node hides its error, un-bypassing restores it - [x] Pasting a bypassed node does not add a new error - [x] Selecting a node filters errors tab to only that node **Subgraph** - [x] Bypassing a subgraph hides interior errors, un-bypassing restores them - [x] Bypassing a node inside a subgraph hides its error, un-bypassing restores it **Workflow switching** - [x] Does not resurface error overlay when switching back to workflow with missing nodes - [x] Restores missing nodes in errors tab when switching back to workflow # Screenshots https://github.com/user-attachments/assets/e0a5bcb8-69ba-4120-ab7f-5c83e4cfc3c5 ## Follow-up work - Extract error-detection computed properties from `RightSidePanel.vue` into a composable (e.g. `useErrorsTabVisibility`) --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
c2dba8f4ee |
chore(#11080): consolidate duplicate rgbToHSL — use shared colorUtil (#11134)
## Summary Consolidate duplicate `rgbToHSL` implementation — mask editor now uses the shared `colorUtil.ts` version instead of its own copy. ## Changes - Export `rgbToHsl` from `src/utils/colorUtil.ts` (was private) - Replace 30-line local `rgbToHSL` in `useCanvasTools.ts` with a 2-line wrapper that imports from `colorUtil.ts` and scales the return values from 0-1 to degree/percentage ## Testing ### Automated - All 176 existing tests pass (`colorUtil.test.ts` + `maskeditor/` suite) - No new tests needed — behavior is identical ### E2E Verification Steps 1. Open any image in the mask editor 2. Select the magic wand / color picker tool 3. Use HSL-based color matching — results should be identical to before ## Review Focus The canonical `rgbToHsl` returns normalized 0-1 values while the mask editor needs degree/percentage scale (h: 0-360, s: 0-100, l: 0-100). The local wrapper handles this conversion. Fixes #11080 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11134-chore-11080-consolidate-duplicate-rgbToHSL-use-shared-colorUtil-33e6d73d36508120bbd8f444f5cc94b6) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com> |
||
|
|
6d4fc1bb10 |
fix: resolve incorrect GLSL live preview for non-primitive widget types (#11010)
## Summary Three issues caused GLSL preview to diverge from backend results: 1. Uniform source resolution always read widgets[0] instead of using link.origin_slot to select the correct widget. Added directValue fallback for widgets not registered in widgetValueStore. 2. Hex color strings (e.g. "#45edf5") were coerced to 0 by Number(). Added hexToInt to colorUtil and used it in toNumber coercion. 3. Custom size_mode was ignored — preview always used upstream image dimensions. Now checks size_mode widget first and respects "custom". ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11010-fix-resolve-incorrect-GLSL-live-preview-for-non-primitive-widget-types-33e6d73d36508101a76bfe8383c0c6ab) by [Unito](https://www.unito.io) |
||
|
|
63eab15c4f |
Range editor (#10936)
BE change https://github.com/Comfy-Org/ComfyUI/pull/13322 ## Summary Add RANGE widget for image levels adjustment - Add RangeEditor widget with three display modes: plain, gradient, and histogram - Support optional midpoint (gamma) control for non-linear midtone adjustment - Integrate histogram display from upstream node outputs ## Screenshots (if applicable) <img width="1450" height="715" alt="image" src="https://github.com/user-attachments/assets/864976af-9eb7-4dd0-9ce1-2f5d7f003117" /> <img width="1431" height="701" alt="image" src="https://github.com/user-attachments/assets/7ee2af65-f87a-407b-8bf2-6ec59a1dff59" /> <img width="705" height="822" alt="image" src="https://github.com/user-attachments/assets/7bcb8f17-795f-498a-9f8a-076ed6c05a98" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10936-Range-editor-33b6d73d365081089e8be040b40f6c8a) by [Unito](https://www.unito.io) |
||
|
|
71a3bd92b4 |
fix: add delete/bookmark actions for blueprints in V2 node library sidebar (#10827)
## Summary Add missing delete and bookmark actions for user blueprints in the V2 node library sidebar, fixing parity with the V1 sidebar. ## Changes - **What**: - Add delete button (inline + context menu) for user blueprints in `TreeExplorerV2Node` and `TreeExplorerV2` - Extract `isUserBlueprint()` helper in `subgraphStore` for DRY usage across V1/V2 sidebars  ## Review Focus - `isUserBlueprint` consolidates logic previously duplicated between `NodeTreeLeaf` and the new V2 components - Context menu guard `contextMenuNode?.data` prevents showing empty menus - Folder `@contextmenu` handler clears stale `contextMenuNode` to prevent wrong actions ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10827-fix-add-delete-bookmark-actions-for-blueprints-in-V2-node-library-sidebar-3366d73d36508111afd2c2c7d8ff0220) by [Unito](https://www.unito.io) |
||
|
|
d9466947b2 |
feat: detect and resolve missing media inputs in error tab (#10309)
## Summary Add detection and resolution UI for missing image/video/audio inputs (LoadImage, LoadVideo, LoadAudio nodes) in the Errors tab, mirroring the existing missing model pipeline. ## Changes - **What**: New `src/platform/missingMedia/` module — scan pipeline detects missing media files on workflow load (sync for OSS, async for cloud), surfaces them in the error tab with upload dropzone, thumbnail library select, and 2-step confirm flow - **Detection**: `scanAllMediaCandidates()` checks combo widget values against options; cloud path defers to `verifyCloudMediaCandidates()` via `assetsStore.updateInputs()` - **UI**: `MissingMediaCard` groups by media type; `MissingMediaRow` shows node name (single) or filename+count (multiple), upload dropzone with drag & drop, `MissingMediaLibrarySelect` with image/video thumbnails - **Resolution**: Upload via `/upload/image` API or select from library → status card → checkmark confirm → widget value applied, item removed from error list - **Integration**: `executionErrorStore` aggregates into `hasAnyError`/`totalErrorCount`; `useNodeErrorFlagSync` flags nodes on canvas; `useErrorGroups` renders in error tab - **Shared**: Extract `ACCEPTED_IMAGE_TYPES`/`ACCEPTED_VIDEO_TYPES` to `src/utils/mediaUploadUtil.ts`; extract `resolveComboValues` to `src/utils/litegraphUtil.ts` (shared across missingMedia + missingModel scan) - **Reverse clearing**: Widget value changes on nodes auto-remove corresponding missing media errors (via `clearWidgetRelatedErrors`) ## Testing ### Unit tests (22 tests) - `missingMediaScan.test.ts` (12): groupCandidatesByName, groupCandidatesByMediaType (ordering, multi-name), verifyCloudMediaCandidates (missing/present, abort before/after updateInputs, already resolved true/false, no-pending skip, updateInputs spy) - `missingMediaStore.test.ts` (10): setMissingMedia, clearMissingMedia (full lifecycle with interaction state), missingMediaNodeIds, hasMissingMediaOnNode, removeMissingMediaByWidget (match/no-match/last-entry), createVerificationAbortController ### E2E tests (10 scenarios in `missingMedia.spec.ts`) - Detection: error overlay shown, Missing Inputs group in errors tab, correct row count, dropzone + library select visibility, no false positive for valid media - Upload flow: file picker → uploading status card → confirm → row removed - Library select: dropdown → selected status card → confirm → row removed - Cancel: pending selection → returns to upload/library UI - All resolved: Missing Inputs group disappears - Locate node: canvas pans to missing media node ## Review Focus - Cloud verification path: `verifyCloudMediaCandidates` compares widget value against `asset_hash` — implicit contract - 2-step confirm mirrors missing model pattern (`pendingSelection` → confirm/cancel) - Event propagation guard on dropzone (`@drop.prevent.stop`) to prevent canvas LoadImage node creation - `clearAllErrors()` intentionally does NOT clear missing media (same as missing models — preserves pending repairs) - `runMissingMediaPipeline` is now `async` and `await`-ed, matching model pipeline ## Test plan - [x] OSS: load workflow with LoadImage referencing non-existent file → error tab shows it - [x] Upload file via dropzone → status card shows "Uploaded" → confirm → widget updated, error removed - [x] Select from library with thumbnail preview → confirm → widget updated, error removed - [x] Cancel pending selection → returns to upload/library UI - [x] Load workflow with valid images → no false positives - [x] Click locate-node → canvas navigates to the node - [x] Multiple nodes referencing different missing files → correct row count - [x] Widget value change on node → missing media error auto-removed ## Screenshots https://github.com/user-attachments/assets/631c0cb0-9706-4db2-8615-f24a4c3fe27d |
||
|
|
c77c8a9476 |
test: migrate fromAny to fromPartial for type-checked test mocks (#10788)
## Summary - Convert `fromAny` → `fromPartial` in 7 test files where object literals or interfaces are passed - `fromPartial` type-checks the provided fields, unlike `fromAny` which bypasses all checking (same as `as unknown as`) - Class-based types (`LGraphNode`, `LGraph`) remain `fromAny` due to shoehorn's `PartialDeep` incompatibility with class constructors ## Changes - **Pure conversions** (all `fromAny` → `fromPartial`): `domWidgetZIndex`, `matchPromotedInput`, `promotionUtils`, `subgraphNavigationStore` - **Mixed** (some converted, some kept): `promotedWidgetView`, `widgetUtil` - **Cleanup**: `nodeOutputStore` type param normalization Follows up on #10761. ## Test plan - [x] `pnpm typecheck` passes - [x] `pnpm vitest run` on all 7 changed files — 169 tests pass - [x] `pnpm lint` passes ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10788-test-migrate-fromAny-to-fromPartial-for-type-checked-test-mocks-3356d73d365081f7bf61d48a47af530c) by [Unito](https://www.unito.io) |
||
|
|
661e3d7949 |
test: migrate as unknown as to @total-typescript/shoehorn (#10761)
*PR Created by the Glary-Bot Agent* --- ## Summary - Replace all `as unknown as Type` assertions in 59 unit test files with type-safe `@total-typescript/shoehorn` functions - Use `fromPartial<Type>()` for partial mock objects where deep-partial type-checks (21 files) - Use `fromAny<Type>()` for fundamentally incompatible types: null, undefined, primitives, variables, class expressions, and mocks with test-specific extra properties that `PartialDeepObject` rejects (remaining files) - All explicit type parameters preserved so TypeScript return types are correct - Browser test `.spec.ts` files excluded (shoehorn unavailable in `page.evaluate` browser context) ## Verification - `pnpm typecheck` ✅ - `pnpm lint` ✅ - `pnpm format` ✅ - Pre-commit hooks passed (format + oxlint + eslint + typecheck) - Migrated test files verified passing (ran representative subset) - No test behavior changes — only type assertion syntax changed - No UI changes — screenshots not applicable ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10761-test-migrate-as-unknown-as-to-total-typescript-shoehorn-3336d73d365081f6b8adc44db5dcc380) by [Unito](https://www.unito.io) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
7864e780e7 |
feat: App mode - Rework save flow (#10439)
## Summary Users were finding the final step of the builder flow confusing/misleading, with the "choose default mode" not actually saving the workflow and people losing changes. This updates it to remove "save"/"set default" as a step in the builder, and changes it to a distinct action. ## Changes - **What**: - add mode selection tab on footer toolbar - extract reusable radio group component - remove setting default mode dialog - add save/save as/saved dialogs ## Screenshots (if applicable) https://github.com/user-attachments/assets/c7439c2e-a917-4f2b-b176-f8bb8c10026d ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10439-feat-App-mode-Rework-save-flow-32d6d73d3650814781b6c7bbea685a97) by [Unito](https://www.unito.io) |
||
|
|
08b1199265 |
test: migrate 13 component tests from VTU to VTL (Phase 1) (#10471)
## Summary Migrate 13 component test files from @vue/test-utils to @testing-library/vue as Phase 1 of incremental VTL adoption. ## Changes - **What**: Rewrite 13 test files (88 tests) to use `render`/`screen` queries, `userEvent` interactions, and `jest-dom` assertions. Add `data-testid` attributes to 6 components for lint-clean icon/element queries. Delete unused `src/utils/test-utils.ts`. - **Dependencies**: `@testing-library/vue`, `@testing-library/user-event`, `@testing-library/jest-dom` (installed in Phase 0) ## Review Focus - `data-testid` additions to component templates are minimal and non-behavioral - PrimeVue passthrough (`pt`) usage in UserAvatar.vue for icon testid - 2 targeted `eslint-disable` in FormRadioGroup.test.ts where PrimeVue places `aria-describedby` on wrapper div, not input ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10471-test-migrate-13-component-tests-from-VTU-to-VTL-Phase-1-32d6d73d36508159a33ffa285afb4c38) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
f1db1122f3 |
fix: allow URI drops to bubble from Vue nodes to document handler (#9463)
## Summary Fix URI drops (e.g. dragging `<img>` thumbnails) onto Vue-rendered nodes by letting unhandled drops bubble to the document-level `text/uri-list` fallback in `app.ts`. ## Changes - **What**: Removed unconditional `.stop` modifier from `@drop` in `LGraphNode.vue`. `stopPropagation()` is now called conditionally — only when `onDragDrop` returns `true` (file drop handled). Made `handleDrop` synchronous since `onDragDrop` returns a plain boolean. ## Review Focus The key insight is that `onDragDrop` (from `useNodeDragAndDrop`) returns `false` synchronously for URI drags (no files in `DataTransfer`), so the event must bubble to reach the document handler that fetches the URI. The original `async` + `await` pattern would have deferred `stopPropagation` past the synchronous propagation phase, so `handleDrop` is now synchronous. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9463-fix-allow-URI-drops-to-bubble-from-Vue-nodes-to-document-handler-31b6d73d36508196a1b3f17e7e4837a9) by [Unito](https://www.unito.io) |
||
|
|
657ae6a6c3 |
fix: subgraph promoted widget input label rename (#10195)
## Summary Promoted primitive subgraph inputs (String, Int) render their link anchor at the header position instead of the widget row. Renaming subgraph input labels breaks the match entirely, causing connections to detach from their widgets visually. ## Changes - **What**: Fix widget-input slot positioning for promoted subgraph inputs in both LiteGraph and Vue (Nodes 2.0) rendering modes - `_arrangeWidgetInputSlots`: Removed Vue mode branch that skipped setting `input.pos`. Promoted widget inputs aren't rendered as `<InputSlot>` Vue components (NodeSlots filters them out), so `input.pos` is the only position fallback - `drawConnections`: Added pre-pass to arrange nodes with unpositioned widget-input slots before link rendering. The background canvas renders before the foreground canvas calls `arrange()`, so positions weren't set on the first frame - `SubgraphNode`: Sync `input.widget.name` with the display name on label rename and initial setup. The `IWidgetLocator` name diverged from `PromotedWidgetView.name` after rename, breaking all name-based slot↔widget matching (`_arrangeWidgetInputSlots`, `getWidgetFromSlot`, `getSlotFromWidget`) ## Review Focus - The `_arrangeWidgetInputSlots` rewrite iterates `_concreteInputs` directly instead of building a spread-copy map — simpler and avoids the stale index issue - `input.widget.name` is now kept in sync with the display name (`input.label ?? subgraphInput.name`). This is a semantic shift from using the raw internal name, but it's required for all name-based matching to work after renames. The value is overwritten on deserialize by `_setWidget` anyway - The `_widget` fallback in `_arrangeWidgetInputSlots` is a safety net for edge cases where the name still doesn't match (e.g., stale cache) Fixes #9998 ## Screenshots <img width="847" height="476" alt="Screenshot 2026-03-17 at 3 05 32 PM" src="https://github.com/user-attachments/assets/38f10563-f0bc-44dd-a1a5-f4a7832575d0" /> <img width="804" height="471" alt="Screenshot 2026-03-17 at 3 05 23 PM" src="https://github.com/user-attachments/assets/3237a7ee-f3e5-4084-b330-371def3415bd" /> <img width="974" height="571" alt="Screenshot 2026-03-17 at 3 05 16 PM" src="https://github.com/user-attachments/assets/cafdca46-8d9b-40e1-8561-02cbb25ee8f2" /> <img width="967" height="558" alt="Screenshot 2026-03-17 at 3 05 06 PM" src="https://github.com/user-attachments/assets/fc03ce43-906c-474d-b3bc-ddf08eb37c75" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10195-fix-subgraph-promoted-widget-input-slot-positions-after-label-rename-3266d73d365081dfa623dd94dd87c718) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: jaeone94 <jaeone.prt@gmail.com> |
||
|
|
20738b6349 |
test: add Vue Testing Library infrastructure and pilot migration (#10319)
## Summary Add Vue Testing Library (VTL) infrastructure and pilot-migrate ComfyQueueButton.test.ts as Phase 0 of an incremental VTL adoption. ## Changes - **What**: Install `@testing-library/vue`, `@testing-library/user-event`, `@testing-library/jest-dom`, and `eslint-plugin-testing-library`. Configure jest-dom matchers globally via `vitest.setup.ts` and `tsconfig.json`. Create shared render wrapper at `src/utils/test-utils.ts` (pre-configures PrimeVue, Pinia, i18n). Migrate `ComfyQueueButton.test.ts` from `@vue/test-utils` to VTL. Add warn-level `testing-library/*` ESLint rules for test files. - **Dependencies**: `@testing-library/vue`, `@testing-library/user-event`, `@testing-library/jest-dom`, `eslint-plugin-testing-library` ## Review Focus - `src/utils/test-utils.ts` — shared render wrapper typing approach (uses `ComponentMountingOptions` from VTU since VTL's `RenderOptions` requires a generic parameter) - ESLint rules are all set to `warn` during migration to avoid breaking existing VTU tests - VTL coexists with VTU — no existing tests are broken ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10319-test-add-Vue-Testing-Library-infrastructure-and-pilot-migration-3286d73d3650812793ccd8a839550a04) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com> |
||
|
|
811d58aef7 |
feat: enable Quiver AI icon for partner nodes (#10366)
## Summary <!-- One sentence describing what changed and why. --> Enable Quiver AI icon for partner nodes ## Changes - **What**: <!-- Core functionality added/modified --> - Enable Quiver AI icon for partner nodes No Quiver AI nodes now, just mock the data to see how it will look <img width="1062" height="926" alt="image" src="https://github.com/user-attachments/assets/1b81a1cc-d72c-413d-bd75-a63925c27a4b" /> ## Review Focus When Quiver AI nodes provided, the icon should show at both node library tree, but also the PreviewCard label. <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10366-feat-enable-Quiver-AI-icon-for-partner-nodes-32a6d73d365081d4801ec2619bd2c77c) by [Unito](https://www.unito.io) |
||
|
|
be6c64c75b |
feat: Add edge panning to ghost node placement (#10308)
## Summary Enables edge panning when placing a ghost-node from the search ## Changes - **What**: - registers document level listeners so dragging over UI elements isnt blocked ## Screenshots (if applicable) https://github.com/user-attachments/assets/c3bda3c5-8255-4dda-a6be-6ef306af2244 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10308-feat-Add-edge-panning-to-ghost-node-placement-3286d73d36508151b645ec3e860bc017) by [Unito](https://www.unito.io) |
||
|
|
77ddda9d3c |
fix: App mode - renaming widgets on subgraphs (#10245)
## Summary Fixes renaming of widgets from subgraph nodes in app builder/app mode. ## Changes - **What**: If the widget is from a subgraph node and no parents passed, use the node as the subgraph parent. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10245-fix-App-mode-renaming-widgets-on-subgraphs-3276d73d3650815bb131c840df43cdf2) by [Unito](https://www.unito.io) |
||
|
|
190bbf0ac2 |
feat: App mode - enable mask editor (#9876)
## Summary Adds the ability for users to access mask editor from app mode ## Changes - **What**: - add mask editor button to app mode load image - extract parseImageWidgetValue for url + test ## Screenshots (if applicable) <img width="303" height="232" alt="image" src="https://github.com/user-attachments/assets/89afffda-d049-4ef4-ba13-bf483222b27c" /> <img width="959" height="715" alt="image" src="https://github.com/user-attachments/assets/db19a327-e53b-4047-aa66-2cb6e889be1d" /> <img width="308" height="302" alt="image" src="https://github.com/user-attachments/assets/efaf601a-c2d8-4098-a3aa-1b5bbb3aec1c" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9876-feat-App-mode-enable-mask-editor-3226d73d365081d5982bf5e0e870f9b3) by [Unito](https://www.unito.io) |
||
|
|
60e8da308f |
fix: resolve nodes in subgraphs for image copy/paste and display (#10009)
## Summary Replace `app.rootGraph.getNodeById()` with `resolveNode()` so node lookups search subgraphs, fixing broken image copy/paste and display for nodes inside subgraphs. ## Changes - **What**: Updated 7 call sites across 5 files to use `resolveNode()` from `litegraphUtil.ts` instead of `app.rootGraph.getNodeById()`. The `resolveNode` function already existed and searches both the root graph and all subgraphs. Added a unit test verifying subgraph node resolution in `nodeOutputStore`. ## Review Focus The fix is mechanical — each call site simply swaps `app.rootGraph?.getNodeById(id)` for `resolveNode(id)`. The `resolveNode` utility iterates `graph.subgraphs` if the node is not found in the root graph. Fixes #9993 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10009-fix-resolve-nodes-in-subgraphs-for-image-copy-paste-and-display-3256d73d3650814f9467c53999f5d755) by [Unito](https://www.unito.io) |
||
|
|
06f7e13957 |
fix: add reve and elevenlabs to icon safelist (#9990)
## Summary Reve and ElevenLabs provider icons were not displaying in the node library because they were missing from the Tailwind icon safelist. ## Changes - **What**: Add `reve` and `elevenlabs` to the `@source inline` safelist in `style.css` so `icon-[comfy--reve]` and `icon-[comfy--elevenlabs]` classes are generated. Add corresponding `PROVIDER_COLORS` entries in `categoryUtil.ts`. <img width="308" height="106" alt="image" src="https://github.com/user-attachments/assets/d488898a-fbad-4af0-8921-0e8ee7d4705a" /> <img width="308" height="78" alt="image" src="https://github.com/user-attachments/assets/2b3b7172-095b-415e-a49a-d303977e0abc" /> ## Review Focus The SVG files already existed in `packages/design-system/src/icons/` but Tailwind's tree-shaking dropped the classes since they're only used dynamically via `getProviderIcon()`. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9990-fix-add-reve-and-elevenlabs-to-icon-safelist-3256d73d36508105994fcdd5d0568027) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
fcdc08fb27 |
feat: structured preload error logging with Sentry enrichment (#8928)
## Summary Add structured preload error logging with Sentry context enrichment and a user-facing toast notification when chunk loading fails (e.g. after a deploy with new hashed filenames). ## Changes - **`parsePreloadError` utility** (`src/utils/preloadErrorUtil.ts`): Extracts structured info from `vite:preloadError` events — URL, file type (JS/CSS/unknown), chunk name, and whether it looks like a hash mismatch. - **Sentry enrichment** (`src/App.vue`): Sets Sentry context and tags on preload errors so they are searchable/filterable in the Sentry dashboard. - **User-facing toast**: Shows an actionable "please refresh" message when a preload error occurs, across all distributions (cloud, desktop, localhost). - **Capture-phase resource error listener** (`src/App.vue`): Catches CSS/script load failures that bypass `vite:preloadError` and reports them to Sentry with the same structured context. - **Unit tests** (`src/utils/preloadErrorUtil.test.ts`): 9 tests covering URL parsing, chunk name extraction, hash mismatch detection, and edge cases. ## Files Changed | File | What | |------|------| | `src/App.vue` | Preload error handler + resource error listener | | `src/locales/en/main.json` | Toast message string | | `src/utils/preloadErrorUtil.ts` | `parsePreloadError()` utility | | `src/utils/preloadErrorUtil.test.ts` | Unit tests | ## Review Focus - Toast fires for all distributions (cloud/desktop/localhost) — intentional so all users see stale chunk errors - `parsePreloadError` is defensive — returns `unknown` for any field it cannot parse - Capture-phase listener filters to only `<script>` and `<link rel="stylesheet">` elements ## References - [Vite preload error handling](https://vite.dev/guide/build#load-error-handling) --------- Co-authored-by: bymyself <cbyrne@comfy.org> |
||
|
|
31a33a0ba2 |
feat: auto-resolve simple validation errors on widget change and slot connection (#9464)
## Summary Automatically clears transient validation errors (`value_bigger_than_max`, `value_smaller_than_min`, `value_not_in_list`, `required_input_missing`) when the user modifies a widget value or connects an input slot, so resolved errors don't linger in the error panel. Also clears missing model state when the user changes a combo widget value. ## Changes - **`useNodeErrorAutoResolve` composable**: watches widget changes and slot connections, clears matching errors via `executionErrorStore` - **`executionErrorStore`**: adds `clearSimpleNodeErrors` and `clearSimpleWidgetErrorIfValid` with granular per-slot error removal - **`executionErrorUtil`**: adds `isValueStillOutOfRange` to prevent premature clearing when a new value still violates the constraint - **`graphTraversalUtil`**: adds `getExecutionIdFromNodeData` for subgraph-aware execution ID resolution - **`GraphCanvas.vue`**: fixes subgraph error key lookup by using `getExecutionIdByNode` instead of raw `node.id` - **`NodeWidgets.vue`**: wires up the new composable to the widget layer - **`missingModelStore`**: adds `removeMissingModelByWidget` to clear missing model state on widget value change - **`useGraphNodeManager`**: registers composable per node - **Tests**: 126 new unit tests covering error clearing, range validation, and graph traversal edge cases ## Screenshots https://github.com/user-attachments/assets/515ea811-ff84-482a-a866-a17e5c779c39 https://github.com/user-attachments/assets/a2b30f02-4929-4537-952c-a0febe20f02e ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9464-feat-auto-resolve-simple-validation-errors-on-widget-change-and-slot-connection-31b6d73d3650816b8afdc34f4b40295a) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
c318cc4c14 |
feat: replace PrimeVue ColorPicker with custom component (#9647)
## Summary - Replace PrimeVue `ColorPicker` with a custom component built on Reka UI Popover - New `ColorPicker` supports HSV saturation-value picking, hue/alpha sliders, hex/rgba display toggle - Simplify `WidgetColorPicker` by removing PrimeVue-specific normalization logic - Add Storybook stories for both `ColorPicker` and `WidgetColorPicker` ## Test plan - [x] Unit tests pass (9 widget tests, 47 colorUtil tests) - [x] Typecheck passes - [x] Lint passes - [ ] Verify color picker visually in Storybook - [ ] Test color picking in node widgets with hex/rgb/hsb formats ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9647-feat-replace-PrimeVue-ColorPicker-with-custom-component-31e6d73d36508114bc54d958ff8d0448) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> |
||
|
|
f5363e4028 |
fix: return undefined for muted node output resolution (#9302)
## Summary Muted (NEVER mode) subgraph nodes throw "No inner node DTO found" during prompt serialization because `resolveOutput()` falls through to subgraph resolution for nodes whose inner DTOs were never registered. ## Changes - **What**: Add early return in `ExecutableNodeDTO.resolveOutput()` for `NEVER` mode nodes, matching the existing `BYPASS` mode guard. Add 5 tests covering muted, bypassed, and normal mode resolution. ## Review Focus The fix is a single-line early return. The key insight is that `graphToPrompt` in `executionUtil.ts` correctly skips `getInnerNodes()` for muted/bypassed nodes, so their inner DTOs are never in the map — but `resolveOutput()` was missing the corresponding guard for `NEVER` mode. Fixes #8986 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9302-fix-return-undefined-for-muted-node-output-resolution-3156d73d3650811e9697c7281f11cf96) by [Unito](https://www.unito.io) |
||
|
|
2f7f3c4e56 |
[feat] Surface missing models in Errors tab (Cloud) (#9743)
## Summary When a workflow is loaded with missing models, users currently have no way to identify or resolve them from within the UI. This PR adds a full missing-model detection and resolution pipeline that surfaces missing models in the Errors tab, allowing users to install or import them without leaving the editor. ## Changes ### Missing Model Detection - Scan all COMBO widgets across root graph and subgraphs for model-like filenames during workflow load - Enrich candidates with embedded workflow metadata (url, hash, directory) when available - Verify asset-supported candidates against the asset store asynchronously to confirm installation status - Propagate missing model state to `executionErrorStore` alongside existing node/prompt errors ### Errors Tab UI — Model Resolution - Group missing models by directory (e.g. `checkpoints`, `loras`, `vae`) with collapsible category cards - Each model row displays: - Model name with copy-to-clipboard button - Expandable list of referencing nodes with locate-on-canvas button - **Library selector**: Pick an alternative from the user's existing models to substitute the missing model with one click - **URL import**: Paste a Civitai or HuggingFace URL to import a model directly; debounced metadata fetch shows filename and file size before confirming; type-mismatch warnings (e.g. importing a LoRA into checkpoints directory) are surfaced with an "Import Anyway" option - **Upgrade prompt**: In cloud environment, free-tier subscribers are shown an upgrade modal when attempting URL import - Separate "Import Not Supported" section for custom-node models that cannot be auto-resolved - Status card with live download progress, completion, failure, and category-mismatch states ### Canvas Integration - Highlight nodes and widgets that reference missing models with error indicators - Propagate missing-model badges through subgraph containers so issues are visible at every graph level ### Code Cleanup - Simplify `surfacePendingWarnings` in workflowService, remove stale widget-detected model merging logic - Add `flattenWorkflowNodes` utility to workflowSchema for traversing nested subgraph structures - Extract `MissingModelUrlInput`, `MissingModelLibrarySelect`, `MissingModelStatusCard` as focused single-responsibility components ## Testing - Unit tests for scan pipeline (`missingModelScan.test.ts`): enrichment, skip-installed, subgraph flattening - Unit tests for store (`missingModelStore.test.ts`): state management, removal helpers - Unit tests for interactions (`useMissingModelInteractions.test.ts`): combo select, URL input, import flow, library confirm - Component tests for `MissingModelCard` and error grouping (`useErrorGroups.test.ts`) - Updated `workflowService.test.ts` and `workflowSchema.test.ts` for new logic ## Review Focus - Missing model scan + enrichment pipeline in `missingModelScan.ts` - Interaction composable `useMissingModelInteractions.ts` — URL metadata fetch, library install, upload fallback - Store integration and canvas-level error propagation ## Screenshots https://github.com/user-attachments/assets/339a6d5b-93a3-43cd-98dd-0fb00681b66f ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9743-feat-Surface-missing-models-in-Errors-tab-Cloud-3206d73d365081678326d3a16c2165d8) by [Unito](https://www.unito.io) |
||
|
|
2ccfb822b4 |
Restore hiding of linked inputs in app mode (#9671)
As a temporary fix for widgets being incorrectly hidden, #9669 allowed all disabled widgets to be displayed. This PR provides a more robust implementation to derive whether the widget, as would be displayed from the root graph, is disabled. Potential regression: - Drag drop handlers are applied on node, not widgets. A subgraph containing a "Load Image" node, does not allow dragging and dropping an image onto the subgraphNode in order to load it. Because app mode widgets would display from the original owning node prior to this PR, these drag/drop handlers would apply. Placing "Load Image" nodes. I believe this change makes behavior more consistent, but it warrants consideration. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9671-Restore-hiding-of-linked-inputs-in-app-mode-31e6d73d365081688e37fbb931f3af68) by [Unito](https://www.unito.io) |