When neither upward nor downward direction has enough space for the
full menu height, clamp the position so the menu stays within the
viewport instead of overflowing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Hide image resolution subtitle on cloud asset cards because thumbnails
are downscaled to max 512px, causing `naturalWidth`/`naturalHeight` to
report incorrect dimensions.
## Changes
- **What**: Gate the dimension display in `MediaAssetCard.vue` behind
`!isCloud` so resolution is only shown on local (where full-res images
are loaded). Added TODO referencing #10590 for re-enabling once
`/assets` API returns original dimensions in metadata.
## Review Focus
One-line conditional change — the `isCloud` import from
`@/platform/distribution/types` follows the established pattern used
across the repo.
Fixes#10590
## Screenshots (if applicable)
N/A — this removes a subtitle that was displaying wrong values (e.g.,
showing 512x512 for a 1024x1024 image on cloud).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10602-fix-hide-inaccurate-resolution-subtitle-on-cloud-asset-cards-3306d73d36508186bd3ad704bd83bf14)
by [Unito](https://www.unito.io)
## Summary
Add the first user-centric Playwright coverage for the assets sidebar
empty state and introduce a small assets-specific test helper/page
object surface.
## Changes
- **What**: add `AssetsSidebarTab`, add `AssetsHelper`, and cover
generated/imported empty states in a dedicated browser spec
## Review Focus
This is intentionally a small first slice for assets-sidebar coverage.
The new helper still mocks the HTTP boundary in Playwright for now
because current OSS job history and input files are global backend
state, which makes true backend-seeded parallel coverage a separate
backend change.
Long-term recommendation: add backend-owned, user-scoped test seeding
for jobs/history and input assets so browser tests can hit the real
routes on a shared backend. Follow-up: COM-307.
Fixes COM-306
## Screenshots (if applicable)
Not applicable.
## Validation
- `pnpm typecheck:browser`
- `pnpm exec playwright test browser_tests/tests/sidebar/assets.spec.ts
--project=chromium` against an isolated preview env
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10595-test-add-assets-sidebar-empty-state-coverage-3306d73d365081d1b34fdd146ae6c5c6)
by [Unito](https://www.unito.io)
## Summary
- Add E2E Playwright tests for zoom controls: default zoom level, zoom
to fit, zoom out with clamping at 10% minimum, manual percentage input,
and toggle visibility
- Add `data-testid` attributes to `ZoomControlsModal.vue` for stable
test selectors
- Add new TestId entries to `selectors.ts`
## Test plan
- [x] All 6 new tests pass locally
- [x] Existing minimap and graphCanvasMenu tests still pass
- [ ] CI passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10589-test-add-browser-tests-for-zoom-controls-3306d73d36508177ae19e16b3f62b8e7)
by [Unito](https://www.unito.io)
## 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>
## Summary
- Add a Playwright-based diagnostic tool (`@audit` tagged) that
automatically detects DOM elements where CSS `contain: layout style`
would improve rendering performance
- Extend `ComfyPage` fixture and `playwright.config.ts` to support
`@audit` tag (excluded from CI, perf infra enabled)
- Add `/contain-audit` skill definition documenting the workflow
## How it works
1. Loads the 245-node workflow in a real browser
2. Walks the DOM tree and scores every element by subtree size and
sizing constraints
3. For each high-scoring candidate, applies `contain: layout style` via
JS
4. Measures rendering performance (style recalcs, layouts, task
duration) before and after
5. Takes before/after screenshots to detect visual breakage
6. Outputs a ranked report to console
## Test plan
- [ ] `pnpm typecheck` passes
- [ ] `pnpm typecheck:browser` passes
- [ ] `pnpm lint` passes
- [ ] Existing Playwright tests unaffected (`@audit` excluded from CI
via `grepInvert`)
- [ ] Run `pnpm exec playwright test
browser_tests/tests/containAudit.spec.ts --project=chromium` locally
with dev server
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10026-tool-add-CSS-containment-audit-skill-and-Playwright-diagnostic-3256d73d365081b29470df164f798f7d)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- Move error red border from TopMenuSection/ComfyActionbar to
ErrorOverlay
- Add error indicator (outline + StatusBadge dot) on right side panel
toggle button when errors are present, the panel/overlay are closed, and
the errors tab setting is enabled
- Replace technical group titles (e.g. "Missing Node Packs") with
user-friendly i18n messages in ErrorOverlay
- Dynamically change action button label based on single error type
(e.g. "Show missing nodes" instead of "See Errors")
- Remove unused `hasAnyError` prop from ComfyActionbar
- Fix `type="secondary"` → `variant="secondary"` on panel toggle button
- Pre-wire `missing_media` error type support for #10309
- Migrate ErrorOverlay E2E selectors from `getByText`/`getByRole` to
`data-testid`
- Update E2E screenshot snapshots affected by TopMenuSection error state
design changes
## Test plan
- [x] Trigger execution error → verify red border on ErrorOverlay, no
red border on TopMenuSection/ComfyActionbar
- [x] With errors and right side panel closed → verify red outline + dot
on panel toggle button
- [x] Open right side panel or error overlay → verify indicator
disappears
- [x] Disable `Comfy.RightSidePanel.ShowErrorsTab` → verify no indicator
even with errors
- [x] Load workflow with only missing nodes → verify "Show missing
nodes" button label and friendly message
- [x] Load workflow with only missing models → verify "Show missing
models" button label and count message
- [x] Load workflow with mixed errors → verify "See Errors" fallback
label
- [x] E2E: `pnpm test:browser:local -- --grep "Error overlay"`
## Screenshots
<img width="498" height="381" alt="스크린샷 2026-03-26 230252"
src="https://github.com/user-attachments/assets/034f0f3f-e6a1-4617-b8f6-cd4c145e3a47"
/>
<img width="550" height="303" alt="스크린샷 2026-03-26 230525"
src="https://github.com/user-attachments/assets/2958914b-0ff0-461b-a6ea-7f2811bf33c2"
/>
<img width="551" height="87" alt="스크린샷 2026-03-26 230318"
src="https://github.com/user-attachments/assets/396e9cb1-667e-44c4-83fe-ab113b313d16"
/>
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Dante <bunggl@naver.com>
## Summary
Fix right-side sidebar panels and left-side panels sharing the same
PrimeVue Splitter state key, causing them to incorrectly apply each
other's saved widths.
## Changes
- **What**: Make `sidebarStateKey` position-aware by including
`sidebarLocation` and offside panel visibility in the localStorage key
## Problem
When sidebar location is set to **right**, all panels (both the
right-side sidebar like Job History and left-side panels like Workflow
overview) share a single PrimeVue Splitter `state-key`
(`unified-sidebar`). PrimeVue persists panel widths to localStorage
using this key, so any resize on one side gets applied to the other.
### AS-IS (before fix)
The `sidebarStateKey` is computed without any awareness of panel
position:
```typescript
// Always returns 'unified-sidebar' (when unified width enabled)
// or the active tab id — regardless of sidebar location or offside panel state
const sidebarStateKey = computed(() => {
return unifiedWidth.value
? 'unified-sidebar'
: (activeSidebarTabId.value ?? 'default-sidebar')
})
```
This produces a **single localStorage key** for all layout
configurations. The result:
1. Set sidebar to **right**, open **Job History** → resize it smaller →
saved to `unified-sidebar`
2. Open **Workflow overview** (appears on the left as an offside panel)
→ loads the same `unified-sidebar` key → gets the Job History width
applied to a completely different panel position
3. Both panels open simultaneously share the same persisted width, even
though they are on opposite sides of the screen
This is exactly the behavior shown in the [issue
screenshots](https://github.com/Comfy-Org/ComfyUI_frontend/issues/9440):
pulling the Workflow overview smaller also changes Job History to that
same size, and vice versa.
### TO-BE (after fix)
The `sidebarStateKey` now includes `sidebarLocation` (`left`/`right`)
and whether the offside panel is visible:
```typescript
const sidebarTabKey = computed(() => {
return unifiedWidth.value
? 'unified-sidebar'
: (activeSidebarTabId.value ?? 'default-sidebar')
})
const sidebarStateKey = computed(() => {
const base = sidebarTabKey.value
const suffix = showOffsideSplitter.value ? '-with-offside' : ''
return `${base}-${sidebarLocation.value}${suffix}`
})
```
This produces **distinct localStorage keys** per layout configuration:
| Layout | Key |
|--------|-----|
| Sidebar left, no offside | `unified-sidebar-left` |
| Sidebar left, right panel open | `unified-sidebar-left-with-offside` |
| Sidebar right, no offside | `unified-sidebar-right` |
| Sidebar right, left panel open | `unified-sidebar-right-with-offside`
|
Each configuration now persists and restores its own panel sizes
independently, so resizing Job History on the right no longer affects
Workflow overview on the left.
## Review Focus
- The offside suffix (`-with-offside`) is necessary because the Splitter
transitions from a 2-panel layout (sidebar + center) to a 3-panel layout
(sidebar + center + offside) — these are fundamentally different panel
configurations and should not share persisted sizes.
Fixes#9440
## Screenshots (if applicable)
See issue for reproduction screenshots:
https://github.com/Comfy-Org/ComfyUI_frontend/issues/9440🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Normalize legacy prefixed proxyWidget entries during subgraph configure
so nested subgraph widgets resolve correctly.
## Changes
- **What**: Extract `normalizeLegacyProxyWidgetEntry` to strip legacy
`nodeId: innerNodeId: widgetName` prefixes from serialized proxyWidgets
and resolve the correct `disambiguatingSourceNodeId`. Write-back
comparison now checks serialized content (not just array length) so
stale formats are cleaned up even when the entry count is unchanged.
## Review Focus
- The iterative prefix-stripping loop in `resolveLegacyPrefixedEntry` —
it peels one `N: ` prefix per iteration and tries all disambiguator
candidates at each level.
- The write-back condition change from length comparison to
`JSON.stringify` equality.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10573-fix-normalize-legacy-prefixed-proxyWidget-entries-on-configure-32f6d73d365081e886e1c9b3939e3b9f)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- When `Comfy.Workflow.Persist` is OFF and storage is empty,
`initializeWorkflow()` returned without creating any workflow tab —
leaving users with no tab and no way to save
- Now falls through to `loadDefaultWorkflow()` so a default temporary
workflow is always created
## Root Cause
In `useWorkflowPersistenceV2.ts`, `initializeWorkflow()` had an early
return when persistence was disabled:
```ts
if (!workflowPersistenceEnabled.value) return
```
This skipped `loadDefaultWorkflow()`, which is responsible for creating
the initial temporary workflow tab via `comfyApp.loadGraphData()` →
`afterLoadNewGraph()` → `workflowStore.createNewTemporary()`.
## Fix
One-line change: `return` → `return loadDefaultWorkflow()`.
## Test plan
- [x] E2E test: verifies `openWorkflows.length >= 1` after reload with
persistence OFF
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10565-fix-create-initial-workflow-tab-when-persistence-is-disabled-32f6d73d365081d5a681c3e019d373c3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Packing nodes inside a subgraph into a nested subgraph no longer blanks
the parent subgraph node's promoted widget values.
## Changes
- **What**: After `convertToSubgraph` moves interior nodes into a nested
subgraph, `_repointAncestorPromotions` rewrites the promotion store
entries on all host SubgraphNodes so they chain through the new nested
node. `rebuildInputWidgetBindings()` then clears the stale
`input._widget` PromotedWidgetView cache and re-resolves bindings from
current connections.
- The root cause was two separate sets of PromotedWidgetView references:
`node.widgets` (rebuilt from the store — correct) vs `input._widget`
(cached at promotion time — stale). `SubgraphNode.serialize()` reads
`input._widget.value`, which resolved against removed node IDs →
`missing-node` → blank values on the next `checkState` cycle.
## Review Focus
- `_repointAncestorPromotions` iterates all graphs to find host nodes of
the current subgraph type — verify this covers all cases (multiple
instances of the same subgraph type).
- `rebuildInputWidgetBindings()` clears `_promotedViewManager` and
re-resolves — confirm no side effects on event listeners or pending
promotions.
- The nested node gets duplicate promotion entries (from both
`_repointAncestorPromotions` and `promoteRecommendedWidgets` via the
`subgraph-converted` event). `store.promote()` deduplicates via
`isPromoted`, but worth verifying.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10532-fix-repoint-ancestor-promoted-widget-bindings-when-packing-nested-subgraphs-32e6d73d365081109d5aea0660434082)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
Co-authored-by: Yourz <crazilou@vip.qq.com>
## Summary
Adds a `@perf` test that establishes a baseline for ResizeObserver
layout cost during zoom on a large graph (245 nodes).
## Changes
- **What**: New `large graph zoom interaction` perf test that zooms
in/out 30 steps on `large-graph-workflow`, measuring `layouts`,
`layoutDurationMs`, `frameDurationMs`, and `TBT`. Each zoom step
triggers ResizeObserver for all node elements due to CSS scale changes.
## Review Focus
This is **PR 1 of 2** for throttling the ResizeObserver during zoom/pan.
Once this merges and establishes a baseline on main, the fix PR (#10473)
will show a CI-proven delta demonstrating the improvement.
The test follows the same patterns as `large graph pan interaction` and
`canvas zoom sweep`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10478-test-add-large-graph-zoom-perf-test-for-ResizeObserver-baseline-32d6d73d365081169537e557c14d8c51)
by [Unito](https://www.unito.io)
## Summary
Fix unhandled promise rejection ("Document is not focused") in Copy
Image and improve clipboard fallback reliability.
## Changes
- **What**: Two clipboard fixes:
1. `litegraphService.ts`: The "Copy Image" context menu passed async
`writeImage` as a callback to `canvas.toBlob()` without awaiting —
errors became unhandled promise rejections reported in [Sentry
CLOUD-FRONTEND-STAGING-AQ](https://comfy-org.sentry.io/issues/6948073569/).
Extracted `convertToPngBlob` helper that wraps `toBlob` in a proper
Promise so errors propagate to the existing outer try/catch and surface
as a user-facing toast instead of a silent Sentry error.
2. `useCopyToClipboard.ts`: Replaced `useClipboard({ legacy: true })`
with explicit modern→legacy fallback that checks
`document.execCommand('copy')` return value. VueUse's `legacyCopy` sets
`copied.value = true` regardless of whether `execCommand` succeeded,
causing false success toasts.
## Review Focus
- The `convertToPngBlob` helper does the same canvas→PNG work as the old
inline code but properly awaited
- The happy path (PNG clipboard write succeeds first try) is unchanged
- No public API surface changes — verified zero custom node dependencies
via ecosystem code search
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9299-fix-handle-clipboard-errors-in-Copy-Image-and-useCopyToClipboard-3156d73d3650817c8608cba861ee64a9)
by [Unito](https://www.unito.io)
## 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)
## Summary
Add regression tests for subgraph slot label propagation. The
OutputSlot.vue fix (adding `slotData.label` to the display template) was
already merged via another PR — this adds tests to prevent future
regressions.
## Changes
- **What**: Two new test files covering the label/localized_name
fallback chain in OutputSlot.vue and SubgraphNode label propagation
through configure() and rename event paths.
## Review Focus
Tests only — no production code changes. Verifies that renamed subgraph
inputs/outputs display correctly in Nodes 2.0 mode.
Fixes#9998
<!-- Pipeline-Ticket: 7d887122-eea5-45f1-b6eb-aed94f708555 -->
## Summary
Remove the unused `_config` parameter from the Playwright global
setup/teardown hooks and drop the now-unused `FullConfig` imports.
## Changes
- **What**: Simplified `browser_tests/globalSetup.ts` and
`browser_tests/globalTeardown.ts` to match actual usage.
## Review Focus
Verify that removing the unused hook argument does not change Playwright
behavior.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10513-fix-remove-unused-Playwright-hook-config-args-32e6d73d365081d59b63dbbca0596025)
by [Unito](https://www.unito.io)
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Adds lessons learned from a bulk backport session where 69 PRs were
admin-merged without CI checks, shipping 3 test failures to core/1.41.
**Changes:**
- **SKILL.md**: CI Safety Rules section, wave verification with `pnpm
test:unit`, continuous backporting recommendation, Never Admin-Merge
Without CI lesson
- **execution.md**: Wait-for-CI step after automation, `gh pr checks
--watch` for manual cherry-picks, CI Failure Triage section with common
failure categories
- **logging.md**: Wave verification log template, CI failure report
table in session report
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10164-chore-add-CI-safety-rules-to-backport-management-skill-3266d73d365081aa856de1fb85a31887)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Prune stale proxyWidgets entries that reference grandchild nodes no
longer present in the outer subgraph after nested packing.
## Changes
- **What**: Filter out proxyWidgets entries during hydration when the
source node doesn't exist in the subgraph. Also skip missing-node
entries in `_pruneStaleAliasFallbackEntries` as defense-in-depth. Write
back cleaned entries so stale data doesn't persist.
## Review Focus
The fix touches two codepaths in `SubgraphNode.ts`:
1. **Hydration** (`_internalConfigureAfterSlots`): Added `getNodeById`
guard before accepting a proxyWidget entry, and broadened the write-back
condition from legacy-only to any filtered entries.
2. **Runtime pruning** (`_pruneStaleAliasFallbackEntries`): Added
early-exit for entries whose source node no longer exists — previously
these survived because failed resolution returned `undefined` which
bypassed the concrete-key comparison.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10390-fix-prune-stale-proxyWidgets-referencing-nodes-removed-by-nested-subgraph-packing-32b6d73d365081e69eedcb2b67d7043d)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Move the `getNodeDefs` unit test out of deprecated `tests-ui` and into
`src/scripts` so Vitest discovers and runs it.
## Changes
- **What**: Renamed `tests-ui/tests/scripts/app.getNodeDefs.test.ts` to
`src/scripts/app.getNodeDefs.test.ts`
## Review Focus
Confirm the spec now follows the colocated test convention and is
included by the existing Vitest `include` globs.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10503-test-move-getNodeDefs-spec-into-src-scripts-32e6d73d3650816f9211dc4c20daba4b)
by [Unito](https://www.unito.io)
## Summary
Use named `dotenv` config imports where we were calling
`dotenv.config()` so ESLint and IDEs stop flagging
`import-x/no-named-as-default-member`.
## Changes
- **What**: Replace default `dotenv` imports plus `.config()` member
access with `import { config as dotenvConfig } from 'dotenv'` in browser
test setup/fixture files and the desktop Vite config.
- **What**: Keep behavior unchanged while aligning those files with the
cleaner import form already used elsewhere in the repo.
## Review Focus
This is a no-behavior-change cleanup. The issue was that `dotenv`
exposes `config` both as a named export and as a property on the
default-exported module object, so `import dotenv from 'dotenv';
dotenv.config()` triggers `import-x/no-named-as-default-member` even
though it works at runtime.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10514-fix-use-named-dotenv-config-imports-32e6d73d36508195b346dbcab764a6b8)
by [Unito](https://www.unito.io)
## Summary
Add layout duration, style recalc duration, and heap usage metrics to CI
perf reports, while improving statistical reliability to reduce false
positive regressions.
## Changes
- **What**:
- Collect `layoutDurationMs`, `styleRecalcDurationMs`, `heapUsedBytes`
(absolute snapshot) alongside existing metrics
- Add effect size gate (`minAbsDelta`) for integer-quantized count
metrics (style recalcs, layouts, DOM nodes, event listeners) — prevents
z=7.2 false positives from e.g. 11→12 style recalcs
- Switch from mean to **median** for PR metric aggregation — robust to
outlier CI runs that dominate n=3 mean
- Increase historical baseline window from **5 to 15 runs** for more
stable σ estimates
- Reorder reported metrics: layout/style duration first (actionable),
counts and heap after (informational)
## Review Focus
The effect size gate in `classifyChange()` — it now requires both z > 2
AND absolute delta ≥ `minAbsDelta` (when configured) to flag a
regression. This addresses the core false positive issue where integer
metrics with near-zero historical variance produce extreme z-scores for
trivial changes.
Median vs mean tradeoff: median is more robust to outliers but less
sensitive to real shifts — acceptable given n=3 and CI noise levels.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10477-perf-add-layout-GC-metrics-reduce-false-positives-in-regression-detection-32d6d73d365081daa72cec96d8a07b90)
by [Unito](https://www.unito.io)
## Summary
App mode templates (names ending in `.app`, e.g.
`templates-qwen_multiangle.app`) were never counted as template
executions in Mixpanel because `getExecutionContext` used
`activeWorkflow.filename` for the `knownTemplateNames` lookup — but
`getFilenameDetails` treats `.app.json` as a compound extension and
strips it entirely, leaving `"templates-qwen_multiangle"` instead of
`"templates-qwen_multiangle.app"`. The set lookup always returned
`false`, so every execution was sent with `is_template: false`.
## Changes
- **Fix**: derive the template lookup key from
`fullFilename.replace(/\.json$/i, '')` instead of `filename`, which
preserves the `.app` suffix and correctly matches `knownTemplateNames`
- **Also fixes**: `workflow_name`, `getTemplateByName`, and
`getEnglishMetadata` calls in the same branch now use the corrected name
- **Tests**: three new cases in `MixpanelTelemetryProvider.test.ts` —
regular template, `.app` template (regression), and non-template
## Before / After
| Template name in index | `activeWorkflow.filename` | `fullFilename` →
stripped | `is_template` |
|---|---|---|---|
| `flux-dev` | `flux-dev` | `flux-dev` | ✅ true |
| `templates-qwen_multiangle.app` | `templates-qwen_multiangle` ❌ |
`templates-qwen_multiangle.app` ✅ | fixed: true |
## Review Focus
The change is confined to `getExecutionContext.ts`. `fullFilename` is
always set (it is assigned in `UserFile` constructor from
`getPathDetails`), so no null-safety issue.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10252-fix-restore-is_template-tracking-for-app-mode-templates-3276d73d365081d4b998edc62ad010dc)
by [Unito](https://www.unito.io)
Apply the same openUpward logic for both teleported and local cases.
When teleported, use bottom CSS property to open upward.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inject OverlayAppendToKey to detect app mode vs canvas. In app mode,
use Teleport to body with position:fixed at the trigger's bottom-right
corner, clamped to viewport bounds. In canvas, keep local absolute
positioning for correct zoom scaling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inject OverlayAppendToKey to detect app mode ('body') vs canvas
('self'). In app mode, use <Teleport to="body"> with position:fixed
to escape overflow-hidden/overflow-y-auto ancestors. In canvas, keep
local absolute positioning for correct zoom scaling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compare space above vs below the trigger and open toward whichever
side has more room. Prevents flipping upward when the menu would
overflow even more in that direction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use getBoundingClientRect() only for direction detection (not
positioning), so it works safely even inside CSS transform chains.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace fragile CSS selectors with data-testid for trigger button
- Update appModeDropdownClipping to use getByTestId after Popover removal
- Change zoom test from 0.5 to 0.75 to avoid too-small click targets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`formatJsonValue()` uses a loose regex `^\d{4}-\d{2}-\d{2}` to detect
date-like strings, which matches non-date strings like
`"2024-01-01-beta"`.
Changes:
- Require ISO 8601 `T` separator: `/^\d{4}-\d{2}-\d{2}T/`
- Validate parse result with `!Number.isNaN(date.getTime())`
- Use `d()` i18n formatter for consistency with `formatDate()` in the
same file
## Summary
Scaffolds the new apps/website/ Astro 5 + Vue 3 marketing site inside
the monorepo.
## Changes
- apps/website/ with package.json, astro.config.mjs, tsconfig, Nx
targets
- @comfyorg/design-system/css/base.css — brand tokens + fonts (no
PrimeVue)
- pnpm-workspace.yaml catalog entries for Astro deps
- .gitignore and env.d.ts for Astro
## Stack (via Graphite)
- **[1/3] Scaffold** ← this PR
- #10141 [2/3] Layout Shell
- #10142 [3/3] Homepage Sections
Part of the comfy.org website refresh (replacing Framer).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10140-feat-scaffold-Astro-5-website-app-design-system-base-css-1-3-3266d73d365081688dcee0220a03eca4)
by [Unito](https://www.unito.io)
## Summary
Restores `getAdditionalUserInfo` from Firebase Auth so sign-up telemetry
fires when *either* Firebase or the UI context identifies a new user,
fixing a regression from #10388.
## Changes
- **What**: In `loginWithGoogle` and `loginWithGithub`, call
`getAdditionalUserInfo(result)` and OR it with the UI-provided
`options?.isNewUser` flag: `is_new_user: options?.isNewUser ||
additionalUserInfo?.isNewUser || false`. Added 8 parameterized unit
tests covering the OR truth table (Firebase true, UI true, both false,
null result).
## Review Focus
The OR semantics: if either source says new user, we send `sign_up`
telemetry. Previously only the UI flag was checked, which missed cases
where the user lands directly on the OAuth provider without going
through the sign-up view.
## Testing
Unit tests cover all branches of the OR logic. An e2e test is not
feasible here because it would require completing a real OAuth flow with
Google/GitHub (interactive popup, valid credentials, CAPTCHA) and
intercepting the resulting `getAdditionalUserInfo` response from
Firebase — none of which can be reliably automated in a headless
Playwright environment without a live Firebase project seeded with
disposable accounts.
Fixes#10447
## Summary
Fix subgraph node slot connector links appearing misaligned after
workflow load, caused by a transform desync between LiteGraph's internal
canvas transform and the Vue TransformPane's CSS transform.
## Changes
- **What**: Changed `syncNodeSlotLayoutsFromDOM` to use DOM-relative
measurement (slot position relative to its parent `[data-node-id]`
element) instead of absolute canvas-space conversion via
`clientPosToCanvasPos`. This makes the slot offset calculation
independent of the global canvas transform, eliminating the frame-lag
desync that occurred when `fitView()` updated `lgCanvas.ds` before the
Vue CSS transform caught up.
- **Cleanup**: Removed the unreachable fallback path that still used
`clientPosToCanvasPos` when the parent node element wasn't found (every
slot element is necessarily a child of a `[data-node-id]` element — if
`closest()` fails the element is detached and measuring is meaningless).
This also removed the `conv` parameter from `syncNodeSlotLayoutsFromDOM`
and `flushScheduledSlotLayoutSync`, and the
`useSharedCanvasPositionConversion` import.
- **Test**: Added a Playwright browser test that loads a subgraph
workflow with `workflowRendererVersion: "LG"` (triggering the 1.2x scale
in `ensureCorrectLayoutScale`) as a template (triggering `fitView`), and
verifies all slot connector positions are within bounds of their parent
node element.
## Review Focus
- The core change is in `useSlotElementTracking.ts` — the new
measurement approach uses `getBoundingClientRect()` on both the slot and
its parent node element, then divides by `currentScale` to get
canvas-space offsets. This is simpler and more robust than the previous
approach.
- SubgraphNodes were disproportionately affected because they are
relatively static and don't often trigger `ResizeObserver`-based
re-syncs that would eventually correct stale offsets.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9121-fix-resolve-subgraph-node-slot-link-misalignment-during-workflow-load-3106d73d365081eca413c84f2e0571d6)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
The custom context menu provided by the frontend exposes widget specific
options. In order to support renaming, promotion, and favoriting, there
needs to be a way to access this context menu when targeting a textarea.
However, always displaying this custom context menu will cause the user
to lose access to browser specific functionality like spell checking,
translation, and the ability to copy paste text.
This PR updates the behaviour so that the native browser context menu
will display when the text area already has focus. Our custom frontend
context menu will continue to display when it does not.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10454-Use-native-context-menu-for-focused-textareas-32d6d73d365081909673d81d6a6ba054)
by [Unito](https://www.unito.io)
Rebased and adopted from #5774 by @felixturner.
## Changes
- Remove unused font-size properties (`NODE_TEXT_SIZE`,
`NODE_SUBTEXT_SIZE`, `DEFAULT_GROUP_FONT`) from theme palettes and color
palette schema
- Replace `DEFAULT_GROUP_FONT`/`DEFAULT_GROUP_FONT_SIZE` with a single
`GROUP_TEXT_SIZE = 20` constant (reduced from 24px)
- Use `NODE_TITLE_HEIGHT` for group header height instead of `font_size
* 1.4`
- Vertically center group title text using `textBaseline = 'middle'`
- Use `GROUP_TEXT_SIZE` directly in TitleEditor instead of per-group
`font_size`
- Remove `font_size` from group serialization (no longer per-group
configurable)
## Original PR
Closes#5774
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9839-feat-Improve-group-title-layout-3216d73d36508112a0edc2a370af20ba)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Felix Turner <felixturner@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Problem
When upgrading from V1 to V2 draft persistence, users' open workflow
tabs were lost. V1 stored tab state (open paths + active index) in
localStorage via `setStorageValue` fallback, but the V1→V2 migration
only migrated draft payloads — not these tab state pointers.
This meant that after upgrading, all previously open tabs disappeared
and users had to manually reopen their workflows.
## Solution
Add `migrateV1TabState()` to the V1→V2 migration path. After draft
payloads are migrated, the function reads the V1 localStorage keys
(`Comfy.OpenWorkflowsPaths` and `Comfy.ActiveWorkflowIndex`) and writes
them to V2's sessionStorage format via `writeOpenPaths()`.
The `clientId` is threaded from `useWorkflowPersistenceV2` (which has
access to `api.clientId`) through to `migrateV1toV2()`.
## Changes
- **`migrateV1toV2.ts`**: Added `migrateV1TabState()` + V1 key constants
for tab state
- **`useWorkflowPersistenceV2.ts`**: Pass `api.clientId` to migration
call
- **`migrateV1toV2.test.ts`**: Two new tests proving tab state migration
works
## Testing
TDD approach — RED commit shows the test failing, GREEN commit shows it
passing.
All 123 persistence tests pass.
- Fixes#9974
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10007-fix-migrate-V1-tab-state-pointers-during-V1-V2-draft-migration-3256d73d36508103b619e521c1b603f5)
by [Unito](https://www.unito.io)
## Summary
The preload error toast fires whenever any custom node extension fails
to load via dynamic `import()`. In practice, this is almost always
caused by third-party plugin bugs rather than ComfyUI core issues.
Common triggers include:
- Bare module specifiers (e.g., `import from "vue"`) that the browser
cannot resolve without an import map
- Incorrect relative paths to `scripts/app.js` due to nested web
directory structures
- Missing dependencies on other extensions (e.g., `clipspace.js`)
Since many users have multiple custom nodes installed, the toast
frequently appears on startup — sometimes multiple times — with a
generic message that offers no actionable guidance. This creates
unnecessary alarm and support burden.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10445-fix-disable-preload-error-toast-triggered-by-third-party-plugin-failures-32d6d73d365081f281efcd6fe90642a5)
by [Unito](https://www.unito.io)
## Summary
Adds custom status messages that are shown under the previews in order
to provide additional progress feedback to the user
Nodes matching the words:
Save, Preview -> Saving
Load, Loader -> Loading
Encode -> Encoding
Decode -> Decoding
Compile, Conditioning, Merge, -> Processing
Upscale, Resize -> Resizing
ToVideo -> Generating video
Specific nodes:
KSampler, KSamplerAdvanced, SamplerCustom, SamplerCustomAdvanced ->
Generating
Video Slice, GetVideoComponents, CreateVideo -> Processing video
TrainLoraNode -> Training
## Changes
- **What**:
- add specific node lookups for non-easily matchable patterns
- add regex based matching for common patterns
- show on both latent preview & skeleton preview
- allow app mode workflow authors to override status with custom
property `Execution Message` (no UI for doing this)
## Review Focus
This is purely pattern/lookup based, in future we could update the
backend node schema to allow nodes to define their own status key.
## Screenshots (if applicable)
<img width="757" height="461" alt="image"
src="https://github.com/user-attachments/assets/2b32cc54-c4e7-4aeb-912d-b39ac8428be7"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10369-feat-App-mode-add-execution-status-messages-32a6d73d3650814e8ca2da5eb33f3b65)
by [Unito](https://www.unito.io)
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Problem
Since PR #8520 (`feat(persistence): fix QuotaExceededError and
cross-workspace draft leakage`), all workflow tabs are lost when the
browser is closed and reopened.
PR #8520 moved tab pointers (`ActivePath`, `OpenPaths`) from
`localStorage` to `sessionStorage` for per-tab isolation. However,
`sessionStorage` is cleared when the browser closes, so the open tab
list is lost on restart. The draft data itself survives in
`localStorage` — only the pointers to which tabs were open are lost.
Reported in
[Comfy-Org/ComfyUI#12984](https://github.com/Comfy-Org/ComfyUI/issues/12984).
Confirmed via binary search: v1.40.9 (last good) → v1.40.10 (first bad).
## Changes
Dual-write tab pointers to both storage layers:
- **sessionStorage** (scoped by `clientId`) — used for in-session
refresh, preserves per-tab isolation
- **localStorage** (scoped by `workspaceId`) — fallback for browser
restart when sessionStorage is empty
Also adds:
- `storageAvailable` guard on write functions for consistency with
`writeIndex`/`writePayload`
- `isValidPointer` validation on localStorage reads to reject stale or
malformed data
## Benefits
- Workflow tabs survive browser restart (restores V1 behavior)
- Per-tab isolation is preserved for in-session use (sessionStorage is
still preferred when available)
## Trade-offs
- On browser restart, the restored tabs come from whichever browser tab
wrote last to localStorage. If Tab A had workflows 1,2,3 and Tab B had
4,5 — the user gets whichever tab wrote most recently. This is the same
limitation V1 had with `Comfy.OpenWorkflowsPaths` in localStorage.
- Previously (post-#8520), opening a new browser tab would only restore
the single most recent draft. With this fix, a new tab restores the full
set of open tabs from the last session. This may be surprising for
multi-tab users who expect a clean slate in new tabs.
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All 121 persistence tests pass
- [x] Manual: open multiple workflow tabs → close browser → reopen →
tabs restored
- [x] Manual: open two browser tabs with different workflows → refresh
each → correct tabs in each
FixesComfy-Org/ComfyUI#12984
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10336-fix-restore-workflow-tabs-on-browser-restart-3296d73d365081b7a7d3e91427d08d17)
by [Unito](https://www.unito.io)
<!-- QA_REPORT_SECTION -->
---
## 🔍 Automated QA Report
| | |
|---|---|
| **Status** | ✅ Complete |
| **Report** |
[sno-qa-10336.comfy-qa.pages.dev](https://sno-qa-10336.comfy-qa.pages.dev/)
|
| **CI Run** | [View
workflow](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/23373697656)
|
Before/after video recordings with **Behavior Changes** and **Timeline
Comparison** tables.
## Summary
Refactor essentials tab node organization to eliminate duplicated logic
and restrict essentials to core nodes only.
## Changes
- **What**:
- Extract `resolveEssentialsCategory` to centralize category resolution
(was duplicated between filter and pathExtractor).
- Add `isCoreNode` guard so third-party nodes never appear in
essentials.
- Replace `indexOf`-based sorting with precomputed rank maps
(`ESSENTIALS_CATEGORY_RANK`, `ESSENTIALS_NODE_RANK`).
<img width="589" height="769" alt="image"
src="https://github.com/user-attachments/assets/66f41f35-aef5-4e12-97d5-0f33baf0ac45"
/>
## Review Focus
- The `isCoreNode` guard in `resolveEssentialsCategory` — ensures only
core nodes can appear in essentials even if a custom node sets
`essentials_category`.
- Rank map precomputation vs previous `indexOf` — functionally
equivalent but O(1) lookup.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10433-refactor-clean-up-essentials-node-organization-logic-32d6d73d36508193a4d1f7f9c18fcef7)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Refactors the error system to improve separation of concerns, fix DDD
layer violations, and address code quality issues.
- Extract `missingNodesErrorStore` from `executionErrorStore`, removing
the delegation pattern that coupled missing-node logic into the
execution error store
- Extract `useNodeErrorFlagSync` composable for node error flag
reconciliation (previously inlined)
- Extract `useErrorClearingHooks` composable with explicit callback
cleanup on node removal
- Extract `useErrorActions` composable to deduplicate telemetry+command
patterns across error card components
- Move `getCnrIdFromNode`/`getCnrIdFromProperties` to
`platform/nodeReplacement` layer (DDD fix)
- Move `missingNodesErrorStore` to `platform/nodeReplacement` (DDD
alignment)
- Add unmount cancellation guard to `useErrorReport` async `onMounted`
- Return watch stop handle from `useNodeErrorFlagSync`
- Add `asyncResolvedIds` eviction on `missingNodesError` reset
- Add `console.warn` to silent catch blocks and empty array guard
- Hoist `useCommandStore` to setup scope, fix floating promises
- Add `data-testid` to error groups, image/video error spans, copy
button
- Update E2E tests to use scoped locators and testids
- Add unit tests for `onNodeRemoved` restoration and double-install
guard
Fixes#9875, Fixes#10027, Fixes#10033, Fixes#10085
## Test plan
- [x] Existing unit tests pass with updated imports and mocks
- [x] New unit tests for `useErrorClearingHooks` (callback restoration,
double-install guard)
- [x] E2E tests updated to use scoped locators and `data-testid`
- [ ] Manual: verify error tab shows runtime errors and missing nodes
correctly
- [ ] Manual: verify "Find on GitHub", "Copy", and "Get Help" buttons
work in error cards
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10302-refactor-error-system-cleanup-store-separation-DDD-fix-test-improvements-3286d73d365081838279d045b8dd957a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- The "Show Advanced Inputs" footer button was missing `headerColor`
style binding, causing it to not sync with the node header color (unlike
the "Enter Subgraph" button which already had it)
- Extracted the repeated `{ backgroundColor: headerColor }` inline style
(4 occurrences) into a `headerColorStyle` computed
## Screenshots
before
<img width="211" height="286" alt="스크린샷 2026-03-24 154312"
src="https://github.com/user-attachments/assets/edfd9480-04fa-4cd4-813d-a95adffbe2d3"
/>
after
<img width="261" height="333" alt="스크린샷 2026-03-24 154622"
src="https://github.com/user-attachments/assets/eab28717-889e-4a6b-8775-bfc08fa727ff"
/>
## Test plan
- [x] Set a custom color on a node with advanced inputs and verify the
footer button matches the header color
- [x] Verify subgraph enter button still syncs correctly
- [x] Verify dual-tab layouts (error + advanced, error + subgraph) both
show correct colors
### Why no E2E test
Node header color is applied as an inline style via `headerColor` prop,
which is already passed and tested through the existing subgraph enter
button path. This change simply extends the same binding to the advanced
inputs buttons — no new data flow or interaction is introduced, so a
screenshot-based E2E test would add maintenance cost without meaningful
regression coverage.
## Summary
Extract duplicated click-vs-drag detection logic into a shared
`useClickDragGuard` composable and `exceedsClickThreshold` pure utility
function.
## Changes
- **What**: New `useClickDragGuard(threshold)` composable in
`src/composables/useClickDragGuard.ts` that stores pointer start
position and checks squared distance against a threshold. Also exports
`exceedsClickThreshold` for non-Vue contexts.
- Migrated `DropZone.vue`, `useNodePointerInteractions.ts`, and
`Load3d.ts` to use the shared utility
- `CanvasPointer.ts` left as-is (LiteGraph internal)
- All consumers now use squared-distance comparison (no `Math.sqrt` or
per-axis `Math.abs`)
## Review Focus
- The composable uses plain `let` state instead of `ref` since
reactivity is not needed for the start position
- `Load3d.ts` uses the pure `exceedsClickThreshold` function directly
since it is a class, not a Vue component
- Threshold values preserved per-consumer: DropZone=5,
useNodePointerInteractions=3, Load3d=5
Fixes#10356
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10357-refactor-extract-shared-click-vs-drag-guard-utility-32a6d73d3650816e83f5cb89872fb184)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Reduce settings dialog size and autofocus search input for better
usability.
## Changes
- **What**: Reduce dialog size from `md` to `sm` (max-width 1400px →
960px); autofocus search input on open
## Review Focus
User feedback indicated the settings dialog was too wide and search
required an extra click to focus.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10396-fix-improve-settings-dialog-UX-32c6d73d365081e29eceed55afde1967)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Add grid view mode for multi-image batches in ImagePreview (Nodes 2.0),
replicating the Nodes 1.0 grid UX where all output images are visible as
clickable thumbnails.
## Changes
- **What**: Multi-image batches now default to a grid view showing all
thumbnails. Clicking a thumbnail switches to gallery mode for that
image. A persistent back-to-grid button sits next to navigation dots,
and hover action bars provide gallery toggle, download, and remove.
Replaced PrimeVue `Skeleton` with shadcn `Skeleton`. Added `viewGrid`,
`viewGallery`, `imageCount`, `galleryThumbnail` i18n keys.
## Review Focus
- Grid column count strategy: fixed breakpoints (2 cols ≤4, 3 cols ≤9, 4
cols 10+) vs CSS auto-fill
- Default view mode: grid for multi-image, gallery for single — matches
Nodes 1.0 behavior
- `object-contain` on thumbnails to avoid cropping (with `aspect-square`
containers for uniform cells)
Fixes#9162
<!-- Pipeline-Ticket: f8f8effa-adff-4ede-b1d3-3c4f04b9c4a0 -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9241-feat-add-grid-view-mode-for-multi-image-batches-in-ImagePreview-3136d73d36508166895ed6c635150434)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- When the cloud backend returns a 403 (user not whitelisted), the
frontend showed a generic "Prompt Execution Error" dialog with a cryptic
message
- Now catches 403 responses specifically and shows an "Access
Restricted" dialog with the backend's actual error message
- Adds `status` field to `PromptExecutionError` so error handlers can
distinguish HTTP status codes
## Changes
- `api.ts`: Added optional `status` to `PromptExecutionError`, pass
`res.status` from `queuePrompt`
- `app.ts`: New `else if` branch in the prompt error handler for `status
=== 403` — shows "Access Restricted" with the backend message
## Backwards compatible
- **Old backend** (`"not authorized"`): Shows "Access Restricted: not
authorized"
- **New backend**
([cloud#2941](https://github.com/Comfy-Org/cloud/pull/2941), `"your
account is not whitelisted for this feature"`): Shows "Access
Restricted: your account is not whitelisted for this feature"
- No behavior change for non-403 errors
## Related
- Backend fix: Comfy-Org/cloud#2941
- Notion: COM-16179
## Test plan
- [ ] Submit a prompt as a non-whitelisted user → should see "Access
Restricted" dialog with clear message
- [ ] Submit a prompt as a whitelisted user → no change in behavior
- [ ] Submit a prompt that fails for other reasons (missing nodes, etc.)
→ existing error handling unchanged
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10402-fix-show-clear-error-dialog-for-403-whitelist-failures-32c6d73d365081eb9528d7feac4e8681)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Matt Miller <mattmiller@Matts-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- When users open/import a workflow with missing nodes, we already track
this via Mixpanel
- This adds a parallel fire-and-forget POST to
`/api/internal/cloud_analytics` so the data also lands in **ClickHouse**
as `frontend:missing_nodes_detected` events
- Payload includes `missing_class_types[]`, `missing_count`, and
`source` (file_button/file_drop/template/unknown)
## Motivation
The frontend is where the **high-value** missing node signal lives —
most users see "missing nodes" and never submit. The backend only
catches the rare case where someone submits anyway. This change captures
both sides.
Companion cloud PR: https://github.com/Comfy-Org/cloud/pull/2886
## Changes
- `MixpanelTelemetryProvider.ts`: Added
`reportMissingNodesToClickHouse()` private method, called from
`trackWorkflowImported()` and `trackWorkflowOpened()`
- Only fires when `missing_node_count > 0`
- Fire-and-forget (`.catch(() => {})`) — no impact on user experience
- Uses existing `api.fetchApi()` which handles auth automatically
## Test plan
- [ ] Open a workflow with missing nodes → verify
`frontend:missing_nodes_detected` event appears in ClickHouse
- [ ] Open a workflow with no missing nodes → verify no event is sent
(check network tab)
- [ ] Verify Mixpanel tracking still works as before
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10132-feat-send-missing-node-data-to-ClickHouse-3266d73d365081559db5ed3efde33e95)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## 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>
## Summary
- Extract all 75 `quickRegister()` mapping entries from
`modelToNodeStore.ts` into a new `modelNodeMappings.ts` constants file
- The store now iterates over the `MODEL_NODE_MAPPINGS` array instead of
having inline calls
- **Zero behavioral change** — same mappings, same order, same runtime
behavior
## Motivation
Adding new model-to-node mappings is currently a code change to the
store. By separating the data into its own file:
- New mappings are a **pure data change** (append a tuple to an array)
- The data file can have its own CODEOWNERS entry, so mapping PRs can be
merged without requiring frontend team review
- Easier to audit — all mappings visible in one place without
interleaved store logic
### Before
```ts
// 250+ lines of quickRegister() calls mixed into store logic
quickRegister('checkpoints', 'CheckpointLoaderSimple', 'ckpt_name')
quickRegister('checkpoints', 'ImageOnlyCheckpointLoader', 'ckpt_name')
// ... 73 more
```
### After
```ts
// modelNodeMappings.ts — pure data
export const MODEL_NODE_MAPPINGS = [
['checkpoints', 'CheckpointLoaderSimple', 'ckpt_name'],
['checkpoints', 'ImageOnlyCheckpointLoader', 'ckpt_name'],
// ...
]
// modelToNodeStore.ts — just iterates
for (const [modelType, nodeClass, key] of MODEL_NODE_MAPPINGS) {
quickRegister(modelType, nodeClass, key)
}
```
## Test plan
- [ ] "Use" button in model browser still works for all model types
- [ ] No regressions in model-to-node resolution (same mappings, same
order)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10237-refactor-extract-model-to-node-mappings-into-separate-data-file-3276d73d365081988656e2ddae772bbc)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Problem
GA4 shows **zero** `sign_up` events from cloud.comfy.org. All new users
(Google/GitHub) are tracked as `login` instead of `sign_up`.
**Root cause:** `getAdditionalUserInfo(result)?.isNewUser` from Firebase
is unreliable for popup auth flows — it compares `creationDate` vs
`lastSignInDate` timestamps, which can differ even for genuinely new
users. When it returns `null` or `false`, the code defaults to pushing a
`login` event instead of `sign_up`.
**Evidence:** GA4 Exploration filtered to `sign_up` + `cloud.comfy.org`
shows 0 events, while `login` shows 8,804 Google users, 519 email, 193
GitHub — all new users are being misclassified as logins.
(Additionally, the ~300 `sign_up` events visible in GA4 are actually
from `blog.comfy.org` — Substack newsletter subscriptions — not from the
app at all.)
## Fix
Use the UI flow context to determine `is_new_user` instead of Firebase's
unreliable API:
- `CloudSignupView.vue` → passes `{ isNewUser: true }` (user is on the
sign-up page)
- `CloudLoginView.vue` → no flag needed, defaults to `false` (user is on
the login page)
- `SignInContent.vue` → passes `{ isNewUser: !isSignIn.value }` (dialog
toggles between sign-in/sign-up)
- Removes the unused `getAdditionalUserInfo` import
## Changes
- `firebaseAuthStore.ts`: `loginWithGoogle`/`loginWithGithub` accept
optional `{ isNewUser }` parameter instead of calling
`getAdditionalUserInfo`
- `useFirebaseAuthActions.ts`: passes the option through
- `CloudSignupView.vue`: passes `{ isNewUser: true }`
- `SignInContent.vue`: passes `{ isNewUser: !isSignIn.value }`
## Testing
- All 32 `firebaseAuthStore.test.ts` tests pass
- All 19 `GtmTelemetryProvider.test.ts` tests pass
- Typecheck passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10388-fix-use-UI-flow-context-for-sign_up-vs-login-telemetry-32b6d73d3650811e96cec281108abbf3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Differentiates the subscription pricing dialog between personal and team
workspaces with distinct visual treatments and a two-stage team
workspace upgrade flow.
### Changes
- **Personal pricing dialog**: Shows "P" avatar badge, "Plans for
Personal Workspace" header, and "Solo use only – Need team workspace?"
banner on each tier card
- **Team pricing dialog**: Shows workspace avatar, "Plans for Team
Workspace" header (emerald), green "Invite up to X members" badge, and
emerald border on Creator card
- **Two-stage upgrade flow**: "Need team workspace?" → closes pricing →
opens CreateWorkspaceDialog → sessionStorage flag → page reload →
WorkspaceAuthGate auto-opens team pricing dialog
- **Spacing**: Reduced vertical gaps/padding/font sizes so the table
fits without scrolling
### Key decisions
- sessionStorage key `comfy:resume-team-pricing` bridges the page reload
during workspace creation
- `onChooseTeam` prop is conditionally passed only to the personal
variant
- `resumePendingPricingFlow()` is called from WorkspaceAuthGate after
workspace initialization
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9901-feat-differentiate-personal-team-pricing-table-with-two-stage-team-workspace-flow-3226d73d365081e7af60dcca86e83673)
by [Unito](https://www.unito.io)
## Summary
FormDropdown Outputs tab only showed the first output for multi-output
jobs because the Jobs API `/jobs` returns a single `preview_output` per
job.
## Changes
- **What**: When history assets include jobs with `outputs_count > 1`,
lazily fetch full outputs via `getJobDetail` (cached in
`jobOutputCache`) and expand them into individual dropdown items.
Single-output jobs are unaffected. Added in-flight guard to prevent
duplicate fetches.
- This is a consumer-side workaround in `WidgetSelectDropdown.vue` that
becomes a no-op once the backend returns all outputs in the list
response (planned Assets API migration).
## Review Focus
- The `resolvedMultiOutputs` shallowRef + watch pattern for async data
feeding into a computed. Each `getJobDetail` call is cached by
`jobOutputCache` LRU, so no redundant network requests.
- This fix is intentionally temporary — it will be superseded when
OSS/cloud both return full outputs from list endpoints.
## No E2E test
E2E coverage is impractical here: reproducing requires a running ComfyUI
backend executing a workflow that produces multiple outputs, then
inspecting the FormDropdown's Outputs tab. The unit test covers the
lazy-loading logic with mocked `getJobDetail` responses.
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Prevent the nightly survey popup from appearing when the feature being
surveyed is currently disabled.
## Root Cause
`useSurveyEligibility` checks the feature usage count from localStorage
but does not verify whether the surveyed feature is currently active.
When a user uses the new node search 3+ times (reaching the survey
threshold), then switches back to legacy search, the usage count
persists in localStorage and the survey popup still appears despite the
feature being off.
## Steps to Reproduce
1. Enable new node search (Settings > Node Search Box Implementation >
"default")
2. Use node search 3+ times (double-click canvas, search for nodes)
3. Switch back to legacy search (Settings > Node Search Box
Implementation > "litegraph (legacy)")
4. Wait 5 seconds on a nightly localhost build
5. **Expected**: No survey popup appears (feature is disabled)
6. **Actual**: Survey popup appears because eligibility only checks
stored usage count, not current feature state
## Changes
1. Added optional `isFeatureActive` callback to `FeatureSurveyConfig`
interface
2. Added `isFeatureActive` guard in `useSurveyEligibility` eligibility
computation
3. Configured `node-search` survey with `isFeatureActive` that checks
the current search box setting
## Red-Green Verification
| Commit | Result | Description |
|---|---|---|
| `99e7d7fe9` `test:` | RED | Asserts `isEligible` is false when
`isFeatureActive` returns false. Fails on current code. |
| `01df9af12` `fix:` | GREEN | Adds `isFeatureActive` check to
eligibility. Test passes. |
Fixes#10333
## 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>
## Summary
Upgrade Nx from 22.5.2 to 22.6.1.
## Changes
- **What**: Bumped nx, @nx/eslint, @nx/playwright, @nx/storybook, and
@nx/vite from 22.5.2 to 22.6.1.
- **Dependencies**: nx, @nx/eslint, @nx/playwright, @nx/storybook,
@nx/vite updated to 22.6.1.
## Review Focus
All Nx migrations ran with no changes needed — this is a straightforward
version bump.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10370-chore-upgrade-nx-22-5-2-22-6-1-32a6d73d36508191988bdc7376dc5e14)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
We've had some reports of issues selecting inputs/nodes when trying to
use the builder in LiteGraph mode and due to the complexity of the
canvas system, we're going to enable Nodes 2.0 when entering the builder
to ensure the best experience.
## Changes
- **What**:
- When entering builder select mode automatically switch to Nodes 2.0
- Extract reusable component from features toast
- Show popup telling user the mode was changed
- Add hidden setting for storing "don't show again" on the switch popup
## Review Focus
- I have not removed the LiteGraph selection code in case someone still
manages to enter the builder in LiteGraph mode, this should be cleaned
up in future
## Screenshots (if applicable)
<img width="423" height="224" alt="image"
src="https://github.com/user-attachments/assets/cc2591bc-e5dc-47ef-a3c6-91ca7b6066ff"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10337-feat-App-mode-Switch-to-Nodes-2-0-when-entering-builder-3296d73d3650818e9f3cdaac59d15609)
by [Unito](https://www.unito.io)
## 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)
## Summary
Add a waveform-based audio player component (`WaveAudioPlayer`)
replacing the native `<audio>` element, with authenticated API fetch for
cloud audio playback.
## Changes
- **What**:
- Add `useWaveAudioPlayer` composable with waveform visualization from
audio data (Web Audio API `decodeAudioData`), playback controls, and
seek support
- Add `WaveAudioPlayer.vue` component with compact (inline waveform +
time) and expanded (full transport controls) variants
- Replace native `<audio>` in `MediaAudioTop.vue` and `ResultAudio.vue`
with `WaveAudioPlayer`
- Use `api.fetchApi()` instead of bare `fetch()` to include Firebase JWT
auth headers, fixing 401 errors in cloud environments
- Add Storybook stories and unit tests
## Review Focus
- The audio URL is fetched via `api.fetchApi()` with auth headers,
converted to a Blob URL, then passed to the native `<audio>` element.
This avoids 401 Unauthorized in cloud environments where `/api/view`
requires authentication.
- URL-to-route extraction logic (`url.includes(apiBase)`) handles both
full API URLs and relative paths.
[screen-capture.webm](https://github.com/user-attachments/assets/44e61812-0391-4b47-a199-92927e75f8b4)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10158-feat-add-WaveAudioPlayer-with-waveform-visualization-and-authenticated-audio-fetch-3266d73d365081beab3fc6274c39fcd4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
- The DraggableList component takes a v-model.
- When a drag is completed, it reassigns to v-model so an update event
fires
- App Builder sets v-model="appModeStore.selectedInputs"
- Thus a completed drag operation effectively performs
appModeStore.selectedInputs = newList
- In appModeStore, selectedInputs is a reactive. Thus, after a drag/drop
operation occurs, selectedInputs in the store is not the same value as
appModeStore.selectedInputs
When a reliable repro for the issue had not yet been found, an attempted
earlier fix for this was to swap from watchers to directly updating
`selectedInputs`. Since this change makes the code cleaner and still
make the code safer, the change is left in place.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10342-Don-t-use-watcher-for-loading-app-mode-selections-3296d73d365081f7b216e79c4f4f4e8d)
by [Unito](https://www.unito.io)
## Summary
- Hide the "Comfy API Key" login button and help text from the sign-in
modal when running on Cloud
- API key auth is only used on local ComfyUI; Cloud has
Firebase-whitelisted Google/GitHub auth
- The button was appearing on Cloud as a relic of shared code between
Cloud and local ComfyUI
## Context
[Discussion with Robin in
#proj-cloud-frontend](https://comfy-organization.slack.com/archives/C09FY39CC3V/p1773977756997559)
— API key auth only works on local because Firebase requires whitelisted
domains. On Cloud, SSO (Google/GitHub) works natively, so the API key
option is unnecessary and confusing.
<img width="470" height="865" alt="Screenshot 2026-03-20 at 9 53 20 AM"
src="https://github.com/user-attachments/assets/5bbdcbaf-243c-48c6-9bd0-aaae815925ea"
/>
## Test plan
- [ ] Verify login modal on local ComfyUI still shows the "Comfy API
Key" button
- [ ] Verify login modal on cloud.comfy.org no longer shows the "Comfy
API Key" button
## Summary
- Add keybinding preset system: save, load, switch, import, export, and
delete named keybinding sets stored via `/api/userdata/keybindings/`
- Preset selector dropdown with "Save Changes" button for modified
custom presets, and "Import keybinding preset" action
- More-options menu in header row with save as new, reset, delete,
import, and export actions
- Search box and menu teleported to settings dialog header (matching
templates modal layout)
- 11 unit tests for preset service CRUD operations
Fixes#1084Fixes#1085
## Test plan
- [ ] Open Settings > Keybinding, verify search box and "..." menu
appear in header
- [ ] Modify a keybinding, verify "Default *" shows modified indicator
- [ ] Use "Save as new preset" from menu, verify preset appears in
dropdown
- [ ] Switch between presets, verify unsaved changes prompt
- [ ] Export preset, import it back, verify bindings restored
- [ ] Delete a custom preset, verify reset to default
- [ ] Verify "Save Changes" button appears only on modified custom
presets
- [ ] Run `pnpm vitest run
src/platform/keybindings/presetService.test.ts`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9681-feat-import-export-keybinding-presets-31e6d73d3650810f88e4d21b3df3e2dd)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Description
On cloud, asset filenames are content hashes (e.g. `16bcbbe5...png`)
while the UI shows `display_name` (e.g. `ComfyUI_00011_`). Fuse.js was
only searching the `name` field, so typing the visible name in the Media
Assets search returned no results.
## Changes
### What
- Add `display_name` to the Fuse.js search keys in
`useMediaAssetFiltering`, so users can search by the name they actually
see in the panel.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10254-fix-search-media-assets-by-display_name-in-addition-to-name-3276d73d3650813dbaf0ccd9d57ccfa3)
by [Unito](https://www.unito.io/)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Add e2e test covering node copy/paste (Ctrl+C/V), image paste onto a
selected LoadImage node, and image paste on empty canvas creating a new
LoadImage node
- Extract `simulateImagePaste` into reusable
`ClipboardHelper.pasteFile()` with auto-detected filename and MIME type
- Add test workflow asset `load_image_with_ksampler.json` and
`image32x32.webp` image asset
## Test plan
- [ ] `pnpm typecheck:browser` passes
- [ ] `pnpm exec eslint browser_tests/tests/copyPaste.spec.ts` passes
- [ ] New test passes in CI: `Copy paste node, image paste onto
LoadImage, image paste on empty canvas`
- [ ] Existing copy/paste tests unaffected
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10233-test-add-copy-paste-node-and-image-paste-e2e-tests-3276d73d365081e78bb9f3f1bf34389f)
by [Unito](https://www.unito.io)
## Summary
Eliminate the double `goto()` in `ComfyPage.setup()` by using
`addInitScript` to seed localStorage before the first navigation.
## Changes
- **What**: Move route mocking and localStorage seeding before `goto()`,
replacing `page.evaluate` with `page.addInitScript` so values are set
before app JS executes on first load. Removes the second `goto()` call
entirely.
## Review Focus
- Verify all Playwright E2E shards pass — the `clearStorage: false` and
`mockReleases: false` paths should be unaffected.
- Note: `addInitScript` persists for the page lifetime (like
`FeatureFlagHelper.seedFlags` already does), so any subsequent
`page.reload()` or `goto()` in tests will also clear storage. This
should be fine since tests that use `clearStorage: false` skip this
block.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10313-perf-eliminate-double-page-navigation-in-Playwright-test-setup-3286d73d36508158a8edeefb27fcae20)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fix workflow loading for nested subgraphs with duplicate node IDs by
configuring subgraph definitions in topological (leaf-first) order.
## Changes
- **What**: Three pre-existing bugs that surface when loading nested
subgraphs with colliding node IDs:
1. Subgraph definitions configured in serialization order — a parent
subgraph's `SubgraphNode.configure` would run before its referenced
child subgraph was populated, causing link/widget resolution failures.
2. `_resolveLegacyEntry` returned `undefined` when `input._widget`
wasn't set yet, instead of falling back to `resolveSubgraphInputTarget`.
3. `_removeDuplicateLinks` removed duplicate links without updating
`SubgraphOutput.linkIds`, leaving stale references that broke prompt
execution.
- **What (housekeeping)**: Moved `subgraphDeduplication.ts` from
`utils/` to `subgraph/` directory where it belongs.
## Review Focus
- Topological sort correctness: Kahn's algorithm with edges from
dependency→dependent ensures leaves configure first. Cycle fallback
returns original order.
- IO slot link repair: `_repairIOSlotLinkIds` runs after
`_configureSubgraph` creates IO slots, patching any `linkIds` that point
to links removed by `_removeDuplicateLinks` during `super.configure()`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10314-fix-configure-nested-subgraph-definitions-in-dependency-order-3286d73d36508171b149e238b8de84c2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
- When in app mode, workflows can be loaded by dragging and dropping as
elsewhere.
- Dragging a file which is supported by a selected app input to the
center panel will apply drop effects on the specific input
- This overrides the loading of workflows
- There's not currently an indicator for where the image will go. This
is being considered for a followup PR
- Outputs can be dragged from the assets panel onto nodes
- This fixes behaviour outside of app mode as well
- Has some thorny implementation specifics
- Non-core nodes may not be able to accept these inputs without an
update
- Node DragOver filtering has reduced functionality when dragging from
the assets pane. Nodes may have the blue border without being able to
accept a drag operation.
- When dropped onto the canvas, the workflow will load (a fix), but the
workflow name will be the url of the image preview
- The entire card is used for the drag preview
<img width="329" height="380" alt="image"
src="https://github.com/user-attachments/assets/2945f9a3-3e77-4e14-a812-4a361976390d"
/>
- Adds a new scroll-shadows tailwind util as an indicator that more
content is available by scrolling.
- Since a primary goal was preventing API costs overflowing, I've made
the indicator fairly strong. This can be tuned later if needed

- Initial support for text outputs in App Mode
- Also causes jobs with text outputs to incorrectly display in the
assets panel with a generic 'check' icon instead of a text specific
icon. This will need a dedicated pass, but shouldn't be overly onerous
in the interim.
<img width="1209" height="735" alt="text output"
src="https://github.com/user-attachments/assets/fcd1cf9f-5d5c-434c-acd0-58d248237b99"
/>
NOTE: Displaying text outputs conflicted with the changes in #9622. I'll
leave text output still disabled in this PR and open a new PR for
reconciling text as an output so it can go through dedicated review.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10122-App-Mode-dragAndDrop-text-output-and-scroll-shadows-3256d73d3650810caaf8d75de94388c9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Set topbar menus to non-modal so they dismiss when clicking
inputs/textareas inside nodes with Nodes 2.0 enabled.
## Changes
- **What**: Add `:modal="false"` to `ContextMenuRoot` in WorkflowTab and
`DropdownMenuRoot` in WorkflowActionsDropdown.

## Review Focus
Modal reka-ui menus set `body.pointer-events: none` and prevent
`focusOutside` dismissal. With Nodes 2.0, widget components use
`@pointerdown.capture.stop` to prevent node dragging, which also blocks
reka-ui's document-level outside-click detection. Non-modal menus allow
`focusin`-based dismissal, which is unaffected by pointerdown stopping.
## Testing
An E2E regression test for this fix requires Nodes 2.0 to be explicitly
enabled (feature-flag guarded), opening a specific topbar menu, and then
clicking inside a canvas node's textarea — an interaction sequence that
has no existing Playwright fixture/helper pattern in the codebase; the
fix itself is a one-line :modal="false" attribute change on reka-ui
primitives whose behavior is documented and tested upstream.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10310-fix-set-topbar-menus-to-non-modal-so-they-dismiss-on-canvas-interaction-3286d73d3650815287d1c66c6ffd4814)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fixes a regression introduced in v1.41.21 where
`_removeDuplicateLinks()` (added by #9120 / backport #10045) incorrectly
removes valid links during workflow loading when the target node has
widget-to-input conversions that shift slot indices.
- Fixes https://github.com/Comfy-Org/workflow_templates/issues/715
## Root Cause
The `_removeDuplicateLinks()` method added in #9120 uses
`node.inputs[link.target_slot]` to determine which duplicate link to
keep. However, `target_slot` is the slot index recorded at serialization
time. During `LGraphNode.configure()`, the `onConnectionsChange`
callback triggers widget-to-input conversions (e.g., KSamplerAdvanced
converting `steps`, `cfg`, `start_at_step`, etc.), which inserts new
entries into the `inputs` array. This shifts indices so that
`node.inputs[target_slot]` no longer points to the expected input.
**Concrete example with `video_wan2_2_14B_i2v.json`:**
The Wan2.2 Image-to-Video subgraph contains a Switch node (id=120)
connected to KSamplerAdvanced (id=85) cfg input. The serialized data has
two links with the same connection tuple `(origin_id=120, origin_slot=0,
target_id=85, target_slot=5)`:
| Link ID | Connection | Status |
|---------|-----------|--------|
| 257 | 120:0 → 85:5 (FLOAT) | Orphaned duplicate (not referenced by any
input.link) |
| 276 | 120:0 → 85:5 (FLOAT) | Valid (referenced by node 85
input.link=276) |
When `_removeDuplicateLinks()` runs after all nodes are configured:
1. KSamplerAdvanced is created with 4 default inputs, but after
`configure()` with widget conversions, it has **13 inputs** (shifted
indices)
2. The method checks `node.inputs[5].link` (target_slot=5 from the
LLink), but index 5 is now a different input due to the shift
3. `node.inputs[5].link === null` → the method incorrectly decides link
276 is not referenced
4. **Link 276 (valid) is removed, link 257 (orphan) is kept** →
connection lost
This worked correctly in v1.41.20 because `_removeDuplicateLinks()` did
not exist.
## Changes
Replace the `target_slot`-based positional lookup with a full scan of
the target node's inputs to find which duplicate link ID is actually
referenced by `input.link`. Also repair `input.link` if it still points
to a removed duplicate after cleanup.
## Test Plan
- [x] Added regression test: shifted slot index scenario
(widget-to-input conversion)
- [x] Added regression test: `input.link` repair when pointing to
removed duplicate
- [x] Existing `_removeDuplicateLinks` tests pass (45/45)
- [x] Full unit test suite passes (6885/6885)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes (0 errors)
- [x] Manual verification: loaded `video_wan2_2_14B_i2v.json` in clean
state — Switch→KSamplerAdvanced cfg link is now preserved
- [ ] E2E testing is difficult for this fix since it requires a workflow
with duplicate links in a subgraph containing nodes with widget-to-input
conversions (e.g., KSamplerAdvanced). The specific conditions —
duplicate LLink entries + slot index shift from widget conversion — are
hard to set up in Playwright without a pre-crafted fixture workflow and
backend node type registration. The unit tests cover the core logic
directly.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10289-fix-_removeDuplicateLinks-incorrectly-removes-valid-link-when-slot-indices-shift-3286d73d36508140b053fd538163e383)
by [Unito](https://www.unito.io)
## Summary
Fix broken link rendering (noodles disappearing or going to wrong
positions) when switching between app mode and graph mode tabs.
## Changes
- **What**: When the graph canvas is hidden via `display: none` in app
mode, slot elements lose valid DOM measurements. On switching back,
links rendered at stale coordinates or disappeared. This PR rekeys
`LGraphNode` components by workflow path, adds measurability guards to
skip hidden slots, clears stale layouts, and watches `linearMode` to
trigger a full slot layout resync on mode transitions.
## Review Focus
- The `isSlotElementMeasurable` guard skips elements that are
disconnected or have zero-size rects — verify this doesn't inadvertently
skip slots during normal graph rendering.
- The `linearMode` watcher clears all slot layouts when entering app
mode and requests a full resync when leaving — confirm no flicker or
race with the RAF-based sync scheduler.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10273-fix-resync-slot-layouts-when-switching-between-app-mode-and-graph-mode-3276d73d3650812f9366dae53c7b2d37)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fire a client-side `subscription_success` event to GTM when a
subscription activates, enabling LinkedIn and Meta conversion tracking
tags.
## Changes
- **What**: Wire `trackMonthlySubscriptionSucceeded()` into both
subscription detection paths (legacy dialog watcher + workspace
billingOperationStore). This method existed but had zero call sites
(dead code). The event name remains `subscription_success` (not
`purchase`) to avoid double-counting with the server-side GA4
Measurement Protocol purchase event and the existing Google Ads Purchase
conversion tag in GTM.
## Review Focus
- billingOperationStore only fires telemetry for `subscription`
operations, not `topup`.
- Legacy flow: telemetry fires in the existing `isActiveSubscription`
watcher, before `emit("close", true)`.
- Workspace flow: telemetry fires in `handleSuccess()` after status
update but before billing context refresh.
- No event name change: `subscription_success` was the original name and
avoids collision with the server-side `purchase` event.
---------
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
## Summary
Fix the bottom-right graph canvas toolbar (zoom controls, fit-to-view,
minimap toggle) not being visible on mobile devices in normal graph
mode.
## Problem
PrimeVue applies `overflow: hidden` to all `.p-splitterpanel` elements
by default. The `GraphCanvasMenu` component is absolutely positioned
(`right-0 bottom-0`) inside the `graph-canvas-panel` SplitterPanel. On
mobile viewports, the panel's bounding box can be smaller than the full
canvas area, causing the toolbar to be clipped by the `overflow:
hidden`.
## Solution
Add `overflow-visible` to the `graph-canvas-panel` SplitterPanel class
to override PrimeVue's default `overflow: hidden`. This allows the
absolutely-positioned toolbar (and minimap) to remain visible regardless
of viewport size.
<img width="873" height="1056" alt="image"
src="https://github.com/user-attachments/assets/7239a5ce-8ce8-4e1d-a8ff-6d6d3c61f5da"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10168-fix-make-graph-canvas-toolbar-visible-on-mobile-3266d73d36508130b675e839cb748fd5)
by [Unito](https://www.unito.io)
## Summary
Fixes a regression introduced in #9680 where groups and nodes could
render in different coordinate spaces when loading legacy
`workflowRendererVersion: "Vue"` workflows with Vue nodes mode enabled.
- Add post-normalization sync to copy normalized LiteGraph node bounds
into `layoutStore` during `loadGraphData`
- Keep sync scoped to Vue nodes mode and only when normalization
actually ran
- Add unit tests for the new layout-store sync helper
- Add Playwright regression coverage for legacy Vue workflow load path
using `groups/nested-groups-1-inner-node`, asserting node/group
centering-gap distances remain within baseline tolerances
## Testing
- pnpm test:unit
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm test:unit
src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.test.ts
- pnpm exec eslint
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm exec oxlint src/scripts/app.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm typecheck
- pnpm typecheck:browser
- pnpm exec eslint browser_tests/tests/vueNodes/groups/groups.spec.ts
- pnpm exec oxlint browser_tests/tests/vueNodes/groups/groups.spec.ts
- pnpm exec playwright test
browser_tests/tests/vueNodes/groups/groups.spec.ts --grep "legacy Vue
workflows"
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10256-fix-resync-vue-node-layout-store-after-legacy-normalization-3276d73d365081568eebc6aa0827d943)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
- On graph change, set the `graph.id` as location hash
- On hash change, navigate to the target `graph.id` either in the
current, or any other loaded workflow.
`canvasStore.currentGraph` does not trigger when `app.loadGraphData` is
called. A trigger could be forced here, but I'm concerned about side
effects. Instead `updateHash` is manually called.
Code search shows that there are no current custom nodes using
`onhashchange`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6811-Allow-graph-navigation-by-browser-forward-backward-2b26d73d365081bb8414fdf7c3686124)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Fix `findProvidersWithFallback` to try all parent paths progressively
instead of jumping from exact match to top-level segment only.
Previously `"a/b/c"` would try `"a/b/c"` then `"a"`, now correctly tries
`"a/b/c"` → `"a/b"` → `"a"`
- Use empty key `''` for `DownloadAndLoadCogVideoControlNet` and
`DownloadAndLoadCogVideoModel` registrations so `shouldUseAssetBrowser`
returns false — these nodes use HuggingFace repo names, not file-based
assets, so the asset browser finds nothing and shows an empty dropdown
## Test plan
- [x] All 46 existing `modelToNodeStore` tests pass
- [ ] Verify CogVideo ControlNet and HF model dropdowns show options
(after backend deploy)
- [x] Verify "Use" button on CogVideo/ControlNet/* models creates the
correct node
- [ ] Verify GGUF asset browser still works
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Copy Url button in MissingModelRow now copies a browsable page URL
instead of a direct download URL
- HuggingFace: `/resolve/` → `/blob/` (file page with model info and
download button)
- Civitai: strips `/api/download` or `/api/v1` prefix (model page)
## Changes
- Add `toBrowsableUrl()` to `missingModelDownload.ts` — converts
download URLs to browsable page URLs for HuggingFace and Civitai
- Update `MissingModelRow.vue` to use `toBrowsableUrl()` when copying
- Add 5 unit tests covering HuggingFace, Civitai, and non-matching URL
cases
## Test plan
- [x] Unit tests pass (14/14) — `toBrowsableUrl` covered by 5 dedicated
tests
- [x] Lint, format, typecheck pass
- [x] Manual: load workflow with missing HuggingFace models, click Copy
Url, verify copied URL opens the file page
- [x] Manual: load workflow with missing Civitai models, click Copy Url,
verify copied URL opens the model page
### Why no E2E test
The Copy Url button is only visible when `!isCloud &&
model.representative.url && !isAssetSupported`. Writing an E2E test for
the clipboard content would require:
1. A test fixture with a real HuggingFace/Civitai URL (fragile — depends
on external availability)
2. Granting `clipboard-read` permission in Playwright context
3. Ensuring `isAssetSupported` evaluates to `false` against the local
test server's node definitions
The URL transformation logic is a pure function, fully covered by unit
tests. E2E clipboard content verification has no existing patterns in
the codebase and would be environment-dependent and flaky.
---------
Co-authored-by: Jin Yi <jin12cc@gmail.com>
## Summary
Address code review feedback from #10134 by renaming the component and
improving implementation quality.
## Changes
- Rename `ResultGallery` → `MediaLightbox` across all references
- Replace `useEventListener(window, 'keydown')` with `@keydown` on
dialog element
- Remove change detector tests (`renders close button`, `prevents
default on arrow keys`)
- Remove redundant `toBeVisible()` before Playwright click (implicit
wait)
- Update keyboard tests to dispatch on dialog element instead of
`window`
- Sort button icon sizes (`icon-sm`, `icon`, `icon-lg`)
- Wire zoom event to lightbox in `MediaAssetCard` story via
`context.args`
- Add standalone `MediaLightbox` Storybook story under
`Platform/Assets/`
Fixes#10134
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10235-refactor-Rename-ResultGallery-to-MediaLightbox-and-address-code-review-3276d73d365081299b42f682373a12f1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Add `quickRegister()` entries for LTX Video prompt enhancer models:
- `LLM/Llama-3.2-3B-Instruct` → `LTXVPromptEnhancerLoader` (`llm_name`)
- `LLM/Florence-2-large-PromptGen-v2.0` → `LTXVPromptEnhancerLoader`
(`image_captioner_name`)
These mappings ensure the "Use" button in the model browser correctly
creates
the appropriate loader node when users click on these LLM models.
## Test plan
- [x] Verify "Use" button works for LLM/Llama-3.2-3B-Instruct in model
browser
- [x] Verify "Use" button works for LLM/Florence-2-large-PromptGen-v2.0
in model browser
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10234-feat-add-model-to-node-mappings-for-LTX-Video-prompt-enhancer-3276d73d36508154847cdbae8c6e9bf1)
by [Unito](https://www.unito.io)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Fix 3D asset disappearing when switching between 3D and image outputs in
app mode — missing `onUnmounted` cleanup leaked WebGL contexts.
## Changes
- **What**: Add `onUnmounted` hook to `Preview3d.vue` that calls
`viewer.cleanup()`, releasing the WebGL context when Vue destroys the
component via its v-if chain. Add unit tests covering init, cleanup on
unmount, and remount behavior.
## Review Focus
When switching outputs in app mode, Vue's v-if chain destroys and
recreates `Preview3d`. Without `onUnmounted` cleanup, the old `Load3d`
instance (WebGL context, RAF loop, ResizeObserver) leaks. After ~8-16
toggles, the browser's WebGL context limit is exhausted and new 3D
viewers silently fail to render.
<!-- Pipeline-Ticket: e36489d2-a9fb-47ca-9e27-88eb3170836b -->
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- WebcamCapture's `serializeValue` was ignoring the `/upload/image`
response and returning the original timestamp-based filename
(`webcam/${timestamp}.png [temp]`)
- This breaks content-addressed storage backends (e.g. Comfy Cloud)
where the server returns a different (hash-based) filename, causing
`ImageDownloadError` at inference time
- Now reads `data.name` and `data.subfolder` from the upload response,
matching the pattern already used by `useNodeImageUpload` and
`useMaskEditorSaver`
- No behavioral change for local ComfyUI — the server already returns
the same filename, it was just being ignored
## Test plan
- [x] WebcamCapture node captures and uploads an image successfully on
local ComfyUI
- [x] WebcamCapture node works correctly on Comfy Cloud (no
`ImageDownloadError`)
- [ ] Duplicate filename edge case: upload two captures with same
timestamp — server-renamed file is used correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10220-fix-use-server-response-filename-in-WebcamCapture-serialization-3266d73d3650818aa9bdff8d27586180)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
`queueStore.update()` previously used a request-ID guard that discarded
responses whenever a newer call had been initiated. During concurrent
job execution, rapid WebSocket events (`status`, `execution_success`,
etc.) created a sustained stream of `update()` calls where:
1. **Every response got discarded** — each returning response saw a
higher `updateRequestId`, so data was never applied to the store until
the event burst subsided
2. **All HTTP requests still fired** — the guard didn't suppress
outgoing calls, just discarded stale responses after the fact
This caused the UI to freeze showing stale job states (e.g. completed
jobs still showing as "Running") during bursts, then snap all at once
when the storm ended.
## Changes
Replace the request-ID guard with a single-flight coalescing pattern:
- **At most one fetch in flight** at a time — no request spam
- **Every response is applied** — no UI starvation
- **Dirty flag** triggers one re-fetch if calls arrived during flight —
eventual consistency
## Testing
Added 5 unit tests covering:
- Concurrent calls coalesce into a single re-fetch
- No response starvation (every response applied)
- No duplicate requests when no concurrent calls
- Loading state transitions through coalesced re-fetches
- Normal sequential behavior preserved
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10203-fix-replace-stale-request-guard-with-single-flight-coalescing-in-queueStore-update-3266d73d365081088656e4e55ca4dbd3)
by [Unito](https://www.unito.io)
## Summary
The `computed` in `usePromotedPreviews` only tracked `nodeOutputs` as a
reactive dependency. GLSL live previews (and other preview-only sources)
write to `nodePreviewImages` instead of `nodeOutputs`, so promoted
preview widgets on SubgraphNodes never re-evaluated when live previews
updated.
## Changes
**Production** (`usePromotedPreviews.ts` — 3-line fix):
- Add `nodePreviewImages[locatorId]` as a second reactive dependency
alongside `nodeOutputs[locatorId]`
- Guard now passes when *either* source has data, not just `nodeOutputs`
**Tests** (`usePromotedPreviews.test.ts`):
- Add `nodePreviewImages` to mock store type and factory
- Add `seedPreviewImages()` helper
- Add `getNodeImageUrls.mockReset()` in `beforeEach` for proper test
isolation
- Two new test cases:
- `returns preview when only nodePreviewImages exist (e.g. GLSL live
preview)`
- `recomputes when preview images are populated after first evaluation`
- Clean up existing tests to use hoisted `getNodeImageUrls` mock
directly instead of `vi.mocked(useNodeOutputStore().getNodeImageUrls)`
## What this supersedes
This is a minimal re-implementation of #9461. That PR also modified
`promotionStore.ts` with a `_version`/`_touch()` monotonic counter to
manually force reactivity — that approach is dropped here as it is an
anti-pattern (manually managing reactivity counters instead of using
Vue's built-in reactivity system). The promotionStore changes were not
needed for this fix.
## Related
- Supersedes #9461
- Prerequisite work: #9198 (add GLSLShader to canvas image preview node
types)
- Upstream feature: #9201 (useGLSLPreview composable)
- Adjacent: #9435 (centralize node image rendering state in
NodeImageStore)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10165-fix-track-nodePreviewImages-in-usePromotedPreviews-for-GLSL-live-preview-propagation-3266d73d365081cd87d0d12c4c041907)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Harden subgraph test coverage: remove low-value change-detector tests,
consolidate fixtures, add behavioral coverage, and fix test
infrastructure issues. Includes minor production code corrections
discovered during test hardening.
## Changes
- **What**: Comprehensive subgraph test suite overhaul across 6 phases
- Removed change-detector tests and redundant assertions
- Consolidated fixture helpers into `subgraphHelpers.ts` /
`subgraphFixtures.ts`
- Added Pinia initialization and fixture reset to all test files
- Fixed barrel import violations (circular dependency prevention)
- Added behavioral coverage for slot connections, events, edge cases
- Added E2E helper and smoke test for subgraph promotion
- Exported `SubgraphSlotBase` from litegraph barrel for test access
- **Production code changes** (minor correctness fixes found during
testing):
- `resolveSubgraphInputLink.ts`: iterate forward (first-connected-wins)
to match `_resolveLinkedPromotionBySubgraphInput`
- `promotionSchema.ts`: return `[]` instead of throwing on invalid
`proxyWidgets`; console.warn always (not DEV-only)
- `LGraph.ts`: disconnect-after-veto ordering fix
- `litegraph.ts`: barrel export swap for `SubgraphSlotBase`
- **Stats**: 349 tests passing, 0 skipped across 26 test files
## Review Focus
- Tests that merely asserted default property values were deleted
(change detectors)
- Fixture state is now reset via `resetSubgraphFixtureState()` in
`beforeEach`
- All imports use `@/lib/litegraph/src/litegraph` barrel to avoid
circular deps
- Production changes are small and directly motivated by test findings
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Migrate two TypeScript lint rules from ESLint to oxlint and upgrade
`eslint-plugin-oxlint` to 1.55.0.
## Changes
- **Rule migration**: Move `typescript/no-import-type-side-effects` and
`typescript/no-empty-object-type` (with `allowInterfaces: 'always'`)
from `eslint.config.ts` to `.oxlintrc.json`.
- **Plugin upgrade**: Bump `eslint-plugin-oxlint` from 1.25.0 to 1.55.0,
which auto-disables additional ESLint rules now covered by oxlint. This
surfaced pre-existing `no-unused-expressions` violations (the `void`
prefixes and ternary-to-if/else changes), which are fixed in a separate
commit.
- **Dependencies**: None — `eslint-plugin-oxlint` auto-disables the
corresponding ESLint rules when they appear in `.oxlintrc.json`.
## Review Focus
- Verify the two migrated rules produce identical diagnostics in oxlint
as they did in ESLint
## Migration Status
See `temp/plans/eslint-to-oxlint-migration.md` for what can/can't
migrate and why.
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Fix nested SubgraphNode input slots doubling on each page reload
- Root cause: during configure, `_configureSubgraph` recreates
`SubgraphInput` objects with new references, and the `input-added` event
handler used `===` identity check which failed for these new objects,
causing `addInput()` to duplicate inputs
- Add `id`-based fallback matching in the `input-added` handler and
rebind `_subgraphSlot` with re-registered listeners
## Changes
**`SubgraphNode.ts:614-622`**: Add UUID `id` fallback to the `===`
reference check in the `input-added` event handler. When a stale
reference is matched by id, call `_addSubgraphInputListeners()` to
update `_subgraphSlot` and re-register listeners on the new
`SubgraphInput` object.
**`SubgraphNode.test.ts`**: 2 regression tests for nested subgraph
reconfigure scenarios.
## Test plan
- [x] Existing SubgraphNode tests pass (6 passed, 34 skipped)
- [x] New tests verify inputs don't duplicate after single and repeated
reconfigure cycles
- [x] Manual: create a subgraph containing another subgraph node, save,
reload — input slots should remain unchanged
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10187-fix-prevent-nested-SubgraphNode-input-slots-from-doubling-on-reload-3266d73d3650817286abea52365a626e)
by [Unito](https://www.unito.io)
## Summary
- Update the E2E test expectation for missing nodes overlay behavior
when switching between opened workflows
- `showMissingNodes` was intentionally changed to `true` in
`workflowService.ts` so users always see missing node pack info when
switching tabs, allowing them to install missing packs without a page
reload
- The test previously asserted the overlay should NOT reappear; now it
asserts the overlay SHOULD reappear
## Test plan
- [x] CI E2E tests pass with updated expectation
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10190-test-update-E2E-to-expect-missing-nodes-overlay-on-tab-switch-3266d73d36508106a48efff7309bd4e5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Add historical trend visualization (ASCII sparklines + directional
arrows) to the performance PR report, showing how each metric has moved
over recent commits on main.
## Changes
- **What**: New `sparkline()`, `trendDirection()`, `trendArrow()`
functions in `perf-stats.ts`. New collapsible "Trend" section in the
perf report showing per-metric sparklines, direction indicators, and
latest values. CI workflow updated to download historical data from the
`perf-data` orphan branch and switched to `setup-frontend` action with
`pnpm exec tsx`.
## Review Focus
- The trend section only renders when ≥3 historical data points exist
(gracefully absent otherwise)
- `trendDirection()` uses a split-half mean comparison with ±10%
threshold — review whether this sensitivity is appropriate
- The `git archive` step in `pr-perf-report.yaml` is idempotent and
fails silently if no perf-history data exists yet on the perf-data
branch
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9939-feat-add-trend-visualization-with-sparklines-to-perf-report-3246d73d36508125a6fcc39612f850fe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Enable virtual nodes (e.g. Set/Get) to resolve their output source
directly when the source lives in a different subgraph.
## Changes
- **What**: Added optional resolveVirtualOutput method on LGraphNode and
a new resolution path in ExecutableNodeDTO.resolveOutput that checks it
before falling through to the existing getInputLink path. Includes unit
tests for the three code paths (happy path, missing DTO, fallthrough).
## Review Focus
- Fully backwards compatible — no existing node implements
resolveVirtualOutput, so the new path is always skipped for current
virtual nodes (Reroute, PrimitiveNode, etc.).
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots
Simple example in actual use, combined with new changes in KJNodes
allows using Get nodes inside subgraphs:
<img width="2242" height="1434" alt="image"
src="https://github.com/user-attachments/assets/cc940a95-e0bb-4adf-91b6-9adc43a74aa2"
/>
<img width="1436" height="440" alt="image"
src="https://github.com/user-attachments/assets/62044af5-0d6e-4c4e-b34c-d33e85f2b969"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10111-feat-resolveVirtualOutput-for-cross-subgraph-virtual-nodes-eg-Set-Get-3256d73d3650816a9f20e28029561c58)
by [Unito](https://www.unito.io)
## 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)
## Summary
Remove the 225px minimum width constraint from reroute nodes so they
render at their intended ~75×26px size.
## Changes
- **What**: Reroute nodes now bypass the `min-w-[225px]` CSS constraint
and bottom padding applied to regular nodes. `resizable: false` is set
on the RerouteNode constructor to hide the resize handle. An
`isRerouteNode` computed in `LGraphNode.vue` gates these behaviors by
checking `nodeData.type === "Reroute"`.
## Review Focus
- Detection mechanism uses `type === "Reroute"` (explicit) rather than
`titleMode === NO_TITLE` (semantic but too broad). See PR #8574 as prior
art for reroute-specific conditionals.
- PR #7993 (open) modifies `LGraphNode.ts` pos/size setters but does not
touch `LGraphNode.vue` template or resize callback — no conflict
expected.
Fixes#4704
## Screenshots (if applicable)
Reroute nodes now render at ~75px wide instead of being forced to 225px
minimum.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8734-fix-vueNodes-decrease-default-size-of-reroute-nodes-3016d73d3650816dbeead93517a52f25)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
- `waitForAuthInitialization` in `api.ts` was silently passing through
without actually waiting for Firebase auth
- `authStore.isInitialized` is unwrapped by Pinia (plain boolean, not a
ref), so `until()` received a static value
- `until()` without `.toBe()` returns a builder object, not a promise —
`Promise.race` treated it as immediately resolved
- Fixed with `storeToRefs` to preserve the ref and `.toBe(true)` to
return an actual promise
## Test plan
- [ ] Verify cloud mode API calls wait for Firebase initialization
before firing
- [ ] Verify non-cloud mode is unaffected
- [ ] Verify no 401s on initial page load in cloud mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10115-fix-broken-Firebase-auth-gate-in-API-layer-3256d73d365081e39b0df4d644f38c84)
by [Unito](https://www.unito.io)
Upgrades `pnpm/action-setup` from v4.2.0 to v4.4.0 across all 16
workflow files and the shared `setup-frontend` action.
## Why
GitHub Actions will force Node.js 24 as the default starting June 2,
2026. The v4.2.0 pin ran on Node.js 20 and emitted deprecation warnings
on every CI run. v4.4.0 was released specifically to address this,
updating the action runtime to Node.js 24.
- Fixes the warning: *"pnpm/action-setup@41ff72... Actions will be
forced to run with Node.js 24 by default starting June 2nd, 2026"*
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10137-ci-upgrade-pnpm-action-setup-to-v4-4-0-Node-js-24-3266d73d36508176b157fcd1d33f2274)
by [Unito](https://www.unito.io)
## WIP — waiting on Typeform ID from Alex Tov (Monday)
Pre-wires `trackFeatureUsed('node-search')` in
`NodeSearchBoxPopover.vue`. Increments a localStorage counter each time
the user opens node search. Currently a no-op because the
`FEATURE_SURVEYS` registry is empty.
**Monday**: Alex provides Typeform ID → add registry entry → survey goes
live on nightly builds.
### What this PR does
- Imports `useSurveyFeatureTracking` composable
- Calls `trackFeatureUsed()` in `showNewSearchBox()`
### What's still needed (will update this PR)
- [x] Add `node-search` entry to `FEATURE_SURVEYS` in
`surveyRegistry.ts` with Typeform ID
- [x] Set up Typeform → Slack webhook to
`#frontend-nightly-user-feedback`
- [ ] Test end-to-end on nightly build
### How the survey system works
After 3 uses of node search on a nightly build, a Typeform survey
popover slides in (once per user, 4-day global cooldown between
surveys). Eligibility: nightly + localhost only, respects opt-out.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9934-WIP-Track-node-search-usage-for-nightly-survey-3246d73d365081308847dd4c0085f21c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- On cloud deployments, the bootstrap sequence was firing authenticated
API calls (`/api/users`, `/api/settings`, `/api/userdata`, `/api/i18n`)
before verifying the user was authenticated via Firebase, causing
repeated 401 responses
- Moves the Firebase auth gate to the top of `startStoreBootstrap()` and
waits for both `isInitialized` **and** `isAuthenticated` before making
any API calls
- The router guard already redirects unauthenticated users to login;
once they authenticate, `isAuthenticated` becomes `true` and the
bootstrap proceeds normally
## Test plan
- [ ] Verify on cloud deployment: unauthenticated users see no 401 API
errors in the network tab
- [ ] Verify on cloud: after login, settings/i18n/workflows load
correctly
- [ ] Verify non-cloud deployments are unaffected (no `isCloud` guard
hit)
- [ ] Unit tests pass (`pnpm test:unit -- --run
src/stores/bootstrapStore.test.ts`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9909-fix-gate-cloud-API-calls-behind-Firebase-authentication-3236d73d3650819287e4e4c68623463b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Replaces two separate PR comment workflows (bundle size + performance)
with a single unified report that posts one combined comment per PR.
## Changes
- **What**: New `pr-report.yaml` aggregator workflow triggers on both
`CI: Size Data` and `CI: Performance Report` completions. Finds sibling
workflow runs by PR head SHA. Renders combined report via
`unified-report.js` (shells out to existing `size-report.js` and
`perf-report.ts`). Sections show "pending" or "failed" placeholders when
data is unavailable.
- **Breaking**: Removes `pr-size-report.yaml` and `pr-perf-report.yaml`.
Legacy `<!-- COMFYUI_FRONTEND_SIZE -->` and `<!-- COMFYUI_FRONTEND_PERF
-->` comments are auto-cleaned on first run.
- **Dependencies**: None
## Review Focus
- Concurrency key uses `head_sha` so the later-completing workflow
cancels the earlier report run, ensuring the final comment always has
both sections.
- Stale-run guard: verifies workflow_run SHA matches the live PR head
before posting.
- The `workflow_dispatch` re-trigger path from `pr-size-report.yaml` is
not carried forward — the unified workflow handles re-trigger naturally
via its dual-trigger design.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9911-feat-unified-PR-report-combining-bundle-size-and-runtime-perf-3236d73d365081baac1cce6f0d9244ac)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Revives #6845. One-time modal introducing Comfy Cloud to macOS desktop
users on first launch, with copy aligned to the latest comfy.org/cloud
page.
## Changes
- **What**: One-time cloud notification modal for macOS + Electron
users, shown 2s after first launch. Includes updated messaging (400 free
monthly credits, no-setup GPU access), telemetry tracking, and UTM
parameters (`utm_id`, `utm_source_platform`).
- **Dependencies**: None
## Review Focus
- Copy alignment with comfy.org/cloud
- Platform guard: macOS + Electron only
- UTM parameters for funnel attribution
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10116-feat-add-cloud-notification-modal-for-macOS-desktop-users-3256d73d36508105995edb71253ae824)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Keep the restored job details popover visible when the job history
sidebar is docked on the left edge of the workspace.
## Changes
- **What**: Replace the fixed `right`-based hover popover positioning
with viewport-aware `left` positioning so the popover opens on the side
with available space, reuse that logic in both `JobAssetsList` and
`QueueJobItem`, and add coverage for left-edge/right-edge placement plus
the job history sidebar integration.
## Review Focus
Please verify the hover popover opens on-screen for left-docked job
history, and that queue overlay / legacy queue row behavior still
matches the intended hover handoff.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9679-fix-keep-job-details-popover-on-screen-in-sidebar-31e6d73d3650816d9e7ffb0749430218)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
Documents 3 hard-learned gotchas from debugging subgraph context menu
E2E tests across ~8 threads:
- Canvas `z-999` overlay intercepting `click()` on DOM widgets → use
`dispatchEvent`
- Context menus requiring node selection before right-click
- Subgraph node sizing minimum for reliable `navigateIntoSubgraph()`
Added to both `browser_tests/AGENTS.md` (auto-loaded for agents working
in that dir) and the Playwright test-writing skill's Common Issues
table.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9951-docs-add-E2E-testing-gotchas-for-canvas-overlay-context-menus-and-subgraph-navigation-3246d73d3650819a928bf91d66e22c2c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Prune orphaned inputs in `_internalConfigureAfterSlots()` to fix
duplicate SubgraphNode inputs that accumulate on serialize-load cycles.
## Changes
- **What**: After `_rebindInputSubgraphSlots()`, filter out inputs with
no matching `_subgraphSlot`. This prevents `LGraphNode.configure()`
`cloneObject` expansion from persisting stale duplicates.
- Added 3 regression tests covering: corrupted serialized data,
reconfigure round-trips, and serialization output.
## Review Focus
The fix is a single `filter()` call. The existing `console.warn` guard
at line ~976 (for inputs without `_subgraphSlot`) becomes dead code
after this fix but is retained as defense-in-depth.
Fixes#9977
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10020-fix-prune-orphaned-SubgraphNode-inputs-after-configure-3256d73d3650812e8cecf4a3c86f2c33)
by [Unito](https://www.unito.io)
## Summary
Extract the shared delayed queue-details hover state into a composable
so the queue overlay and grouped queue rows use the same timing and
handoff behavior.
## Changes
- **What**: Add `useJobDetailsHover`, migrate `JobAssetsList` and
`JobGroupsList` to it, convert the touched helper functions to
declarations, and add a grouped-row regression test for the A -> B ->
leave stale-popover case.
## Review Focus
Verify that details hover timing stays consistent between the queue
overlay and grouped queue rows, especially around the A -> B hover
handoff and brief-hover exit path.
Stacked on #9549.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9924-fix-share-queue-details-hover-state-3236d73d36508108aca5dcfb99bf33c6)
by [Unito](https://www.unito.io)
## Summary
Fix missing capture button on WebcamCapture node in Vue renderer mode.
## Changes
- **What**: Remove `canvasOnly: true` from the capture button widget
options. This flag prevented `WidgetButton.vue` from rendering the
button. The WEBCAM DOM widget (video feed) already renders correctly via
`WidgetDOM.vue`.
## Review Focus
Single-line change: `{ canvasOnly: true }` → `{}`. The button's label,
callback, disabled state, and serializeValue behavior are all handled by
WidgetButton.vue already.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9936-fix-show-webcam-capture-button-in-Vue-renderer-3246d73d36508173a6e5ca59d9fb7af1)
by [Unito](https://www.unito.io)
## Summary
- Move `contain-layout contain-style` from the outer node container to
the inner wrapper to prevent CSS containment from interfering with
inter-node compositing
- Add `isolation: isolate` to the outer container for explicit stacking
context isolation
- Fixes#9988
## Root Cause
Originally, PR
[#5169](https://github.com/Comfy-Org/ComfyUI_frontend/pull/5169)
(`57db10f`, 2025-08-23) applied all three containment properties to the
outer node container:
```
contain-layout contain-style contain-paint
```
`contain-paint` ensured each node's painting was clipped to its own
boundaries, guaranteeing render isolation between sibling nodes.
Later, PR
[#6289](https://github.com/Comfy-Org/ComfyUI_frontend/pull/6289)
(`6e4471ad`, 2025-10-28) removed `contain-paint` to fix a node sizing
issue — likely because `contain-paint` clips content to the element's
padding box, which would clip selection overlays and footer tabs that
extend beyond the node boundary via negative insets.
Removing `contain-paint` while keeping `contain-layout contain-style` on
the outer container broke paint isolation between nodes. Without paint
containment, the browser's compositor no longer treated each node as an
isolated painting unit, allowing text from lower z-index nodes to
visually bleed through higher z-index nodes.
## Fix
1. **Move containment inward**: `contain-layout contain-style` is moved
from the outer container to the inner wrapper (`node-inner-wrapper`).
This preserves layout/style containment performance benefits while
keeping them scoped to the node body — away from the stacking context
boundary between sibling nodes.
2. **Explicit isolation**: `isolation: isolate` is added to the outer
container. While `transform` + `z-index` already create a stacking
context, `isolation: isolate` provides an unambiguous signal to the
browser's compositor to treat each node as an isolated compositing unit
— replacing the paint isolation role that `contain-paint` previously
served, without clipping overlays.
## Red-Green Verification
| Commit | Status | Purpose |
|--------|--------|---------|
|
[`feb8555`](https://github.com/Comfy-Org/ComfyUI_frontend/commit/feb855501)
`test:` | 🔴 Red (verified locally) | Test asserts `isolate`
on outer container and `contain-layout contain-style` on inner wrapper
only — fails against current main |
|
[`c47f89e`](https://github.com/Comfy-Org/ComfyUI_frontend/commit/c47f89ed2)
`fix:` | 🟢 Green (CI: Tests Unit passing) | Moves
containment to inner wrapper, adds `isolation: isolate` — test passes |
**Local red verification:**
```
AssertionError: expected [ 'group/node', 'lg-node', …(9) ] to include 'isolate'
❯ src/renderer/extensions/vueNodes/components/LGraphNode.test.ts:264:26
```
## Test Plan
- [x] CI red on test-only commit (verified locally — test fails without
fix)
- [x] CI green on fix commit (CI: Tests Unit passes)
- [ ] Place a String Literal node with long text on the canvas
- [ ] Place a Load Image node (with an image loaded) overlapping the
String Literal
- [ ] Verify text from the String Literal does NOT bleed through the
Load Image node
- [ ] Verify node selection outlines, error borders, and footer tabs
render correctly
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Read `sentry_dsn` from remote config at runtime for cloud builds,
following the same pattern as `firebase.ts` and `comfyApi.ts`
- Upload sourcemaps to both staging (`SENTRY_PROJECT`) and prod
(`SENTRY_PROJECT_PROD`) Sentry projects at build time
- Remove `SENTRY_DSN_PROD` / `USE_PROD_CONFIG`-based DSN selection — DSN
is now delivered per-environment via dynamic config
## Companion PR
- https://github.com/Comfy-Org/cloud/pull/2883 (cloud repo: dynamic
config + workflow)
## Setup required
- Add `SENTRY_PROJECT_PROD` variable (`cloud-frontend-prod`) to GitHub
STAGING environment in cloud repo
## Test plan
- [ ] Verify staging build reads staging DSN from remote config
- [ ] Verify prod build reads prod DSN from remote config
- [ ] Confirm sourcemaps are uploaded to both Sentry projects
- [ ] Validate fallback to `__SENTRY_DSN__` when remote config is
unavailable
## Summary
Fix high-resolution image preview expanding to full screen width, hiding
the close button.
## Changes
- **What**: Override scoped `ComfyImage` styles (`width/height: 100%`,
`object-fit: cover`) in the gallery context with `auto` sizing and
`max-width: 100vw; max-height: 100vh` constraints. The scoped
`[data-v-xxx]` attribute selector gave `comfy-image-main` higher
specificity than the existing `img.galleria-image` rule, so the new
selector `img.comfy-image-main.galleria-image` is used to win
specificity.
## Review Focus
- CSS specificity approach: using `.comfy-image-main.galleria-image` to
override Vue scoped styles without `!important`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10129-fix-high-res-image-preview-overflowing-screen-and-hiding-close-button-3266d73d36508191a03bcc75f9912421)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Remove the legacy missing nodes modal dialog and migrate all
functionality to the existing Error Overlay / TabErrors system
- Migrate core node version warning from `MissingCoreNodesMessage.vue`
to `MissingNodeCard` in the errors tab
- Remove `Comfy.Workflow.ShowMissingNodesWarning` setting (errors tab
always surfaces missing nodes)
- Delete 6 legacy files: `useMissingNodesDialog.ts`,
`MissingNodesContent.vue`, `MissingNodesFooter.vue`,
`MissingNodesHeader.vue`, `MissingCoreNodesMessage.vue` and its test
- Rename `showMissingNodesDialog`/`showMissingModelsDialog` params to
`showMissingNodes`/`showMissingModels`
- Add `errorOverlay` and `missingNodeCard` to centralized `TestIds`
- Migrate all E2E tests from legacy dialog selectors to error overlay
testIds
- Add new E2E test: MissingNodeCard visible via "See Errors" button flow
- Add new E2E test: subgraph missing node type verified by expanding
pack row
- Add `surfaceMissingNodes` unit tests to `executionErrorStore`
- Guard `semver.compare` against invalid version strings
- Add `role="alert"`, `aria-hidden` for accessibility
- Use reactive props destructuring in `MissingNodeCard` and
`MissingPackGroupRow`
**Net change: -669 lines** (19 files, +323 / -992)
<img width="733" height="579" alt="image"
src="https://github.com/user-attachments/assets/c497809d-b176-43bf-9872-34bd74c6ea0d"
/>
## Test plan
- [x] Unit tests: MissingNodeCard core node warning (7 tests)
- [x] Unit tests: surfaceMissingNodes (4 tests)
- [x] Unit tests: workflowService showPendingWarnings (updated)
- [x] E2E: Error overlay visible on missing nodes workflow
- [x] E2E: Error overlay visible on subgraph missing nodes
- [x] E2E: MissingNodeCard visible via See Errors button
- [x] E2E: Subgraph node type visible after expanding pack row
- [x] E2E: Error overlay does not resurface on undo/redo
- [x] E2E: Error overlay does not reappear on workflow tab switch
- [x] Typecheck, lint, knip all passing
## Related issues
- Closes#9923 (partially — `errorOverlay` and `missingNodeCard` added
to TestIds)
- References #10027 (mock hoisting inconsistency)
- References #10033 (i18n-based test selectors)
- References #10085 (DDD layer violation + focusedErrorNodeId)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10102-refactor-remove-legacy-missing-nodes-dialog-3256d73d365081c194d2e90bc6401846)
by [Unito](https://www.unito.io)
Resolve all lint warnings (3 oxlint + 1 eslint).
## Changes
- Replace `it.todo` with `it.skip` in subgraph tests (`warn-todo`)
- Move `vi.mock` to top-level in `firebaseAuthStore.test.ts`
(`hoisted-apis-on-top`)
- Rename `DOMPurify` default import to `dompurify`
(`no-named-as-default`)
---
### The Villager Who Ignored the Warnings
Once there lived a villager whose compiler whispered of lint. "They are
only *warnings*," she said, and went about her day. One warning became
three. Three became thirty. The yellow text grew like ivy across the
terminal, until no one could tell the warnings from the errors. One
morning a real error appeared — a misplaced mock, a shadowed import —
but nobody noticed, for the village had long since learned to stop
reading. The build shipped. The users wept. And the warning, faithful to
the last, sat quietly in the log where it had always been.
*Moral: Today's warning is tomorrow's incident report.*
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9972-fix-resolve-all-lint-warnings-3246d73d3650810a89cde5d05e79d948)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Restrict PyPI publishing of `comfyui-frontend-package` to bi-weekly
ComfyUI release cycles only, instead of every nightly version bump.
## Changes
- **What**: Move `publish_pypi` job from `release-draft-create.yaml` to
`release-biweekly-comfyui.yaml`
1. Removed `publish_pypi` job from `release-draft-create.yaml` (no
longer publishes on every merged Release PR)
2. Added `publish-pypi` job to `release-biweekly-comfyui.yaml` with tag
polling, build, publish, and PyPI availability confirmation
3. Gated `create-comfyui-pr` on `publish-pypi` success so the ComfyUI
requirements bump PR is only created after the package is confirmed
available
4. Updated ComfyUI PR body to confirm PyPI availability instead of
warning about a pending release PR
- **Breaking**: None — nightly releases still create GitHub releases and
publish npm types; only PyPI publishing timing changes
- **Dependencies**: None
## Review Focus
- The `publish-pypi` job uses `if: always() &&
needs.resolve-version.result == 'success'` to run even when
`trigger-release-if-needed` is skipped (tag already exists)
- Tag polling (30min timeout) waits for the version bump PR to be merged
before building from the tagged commit
- PyPI propagation polling (15min timeout) confirms the package is
installable before creating the ComfyUI PR
Fixes COM-16778
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9948-ops-restrict-PyPI-publishing-to-bi-weekly-ComfyUI-releases-3246d73d36508198b00fcc247ac5b58c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Adds `gh pr merge --auto --squash` after backport PR creation in the
backport workflow, so backport PRs merge automatically once checks pass
- Uses `|| echo "::warning::..."` fallback to avoid failing the workflow
if auto-merge can't be enabled (e.g. repo setting not configured)
## Test plan
- [ ] Trigger backport workflow on a test PR with `needs-backport` label
- [ ] Verify auto-merge is enabled on the created backport PR
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10108-chore-enable-auto-merge-on-backport-PRs-3256d73d3650814eb6e5fb2bdf3c5ec7)
by [Unito](https://www.unito.io)
## Summary
Restore the existing job details popover in the expanded queue overlay
after the list row UI moved to `AssetsListItem`.
## Changes
- **What**: Add delayed hover-driven job details popover support back to
`JobAssetsList`, including teleported positioning, row-to-popover hover
handoff, and focused component coverage for the timing behavior.
## Review Focus
Please verify the hover transition between the queue row and the
teleported popover, especially around positioning and whether the
popover remains reachable in the top menu queue overlay.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9549-fix-restore-queue-job-details-popover-31d6d73d3650815eaf45dcc2a14d3dee)
by [Unito](https://www.unito.io)
## Summary
Remove team assignments from CODEOWNERS to reduce notification noise for
internal PRs. Add a workflow that requests team review only when
external contributors open PRs.
## Changes
- **What**: Strip `@Comfy-org/comfy_frontend_devs` and
`@Comfy-Org/comfy_maintainer` from all CODEOWNERS entries (keep
individual user assignments). Add `pr-request-team-review.yaml` workflow
that uses `pull_request_target` to request team review for
non-collaborator PRs.
- **Dependencies**: None
## Review Focus
- The workflow uses `pull_request_target` but does not check out or
execute any untrusted code — it only runs `gh pr edit --add-reviewer`.
- The `author_association` check excludes OWNER, MEMBER, and
COLLABORATOR — internal PRs will not trigger team review requests.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10104-chore-replace-team-CODEOWNERS-with-external-PR-review-workflow-3256d73d3650813b887ac16b5e97b4c4)
by [Unito](https://www.unito.io)
## Summary
- Adds a **Paste Image** context menu option to nodes that support image
pasting (e.g. Load Image), complementing the existing **Copy Image**
option
- Reads clipboard image data via `navigator.clipboard.read()` and
delegates to `node.pasteFiles()` — same path as `Ctrl+V` paste
- Only shown on nodes that implement `pasteFiles` (e.g. LoadImage)
- Available even when no image is loaded yet (e.g. fresh LoadImage node)
- **Node 2.0 context menu only** — legacy litegraph menu is not
supported
- Fixes#9989
<img width="852" height="685" alt="스크린샷 2026-03-16 오후 5 34 28"
src="https://github.com/user-attachments/assets/219e8162-312a-400b-90ec-961b95b5f472"
/>
## Test plan
- [x] Right-click a Load Image node (with or without image loaded) →
verify "Paste Image" appears
- [x] Copy an image to clipboard → click "Paste Image" → verify image is
loaded into the node
- [x] Verify "Paste Image" does not appear on output-only image nodes
(e.g. Save Image, Preview Image)
- [x] Unit tests pass: `pnpm vitest run
src/composables/graph/useImageMenuOptions.test.ts`
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Documentation Audit - File Path Updates
## Summary
Conducted a comprehensive audit of all documentation files across the
repository to ensure 100% accuracy of file paths, code references, and
technical information. Fixed outdated file path references that resulted
from codebase restructuring.
## Changes Made
### File Path Corrections
**docs/SETTINGS.md**
- Updated `src/constants/coreSettings.ts` →
`src/platform/settings/constants/coreSettings.ts` (3 occurrences)
- Updated `src/stores/settingStore.ts` →
`src/platform/settings/settingStore.ts` (2 occurrences)
- Updated `src/services/newUserService.ts` →
`src/services/useNewUserService.ts` (1 occurrence)
**src/locales/CONTRIBUTING.md**
- Updated `src/constants/coreSettings.ts` →
`src/platform/settings/constants/coreSettings.ts` (1 occurrence)
**docs/testing/unit-testing.md**
- Updated `@/domains/workflow/validation/schemas/workflowSchema` →
`@/platform/workflow/validation/schemas/workflowSchema` (1 occurrence)
## Audit Results
### Files Audited (✅ All Pass)
- ✅ Core documentation: README.md, CONTRIBUTING.md, CLAUDE.md, AGENTS.md
- ✅ Docs directory: 27 files in docs/**/*.md
- ✅ Claude commands: 7 files in .claude/commands/*.md
- ✅ Locales documentation: src/locales/CONTRIBUTING.md
- ✅ Package.json scripts: All documented commands verified against
actual scripts
### Accuracy Statistics
- **Total file path references audited:** 50+
- **Broken references found:** 5
- **Broken references fixed:** 5
- **Documentation files scanned:** 31
- **Final accuracy rate:** 100%
### Verified as Accurate
- ✅ All package.json script references in AGENTS.md match actual scripts
- ✅ All configuration file references (.vscode/extensions.json,
vite.config.mts, etc.) point to existing files
- ✅ All code examples reference actual existing files
- ✅ All ADR (Architecture Decision Records) dates and references are
correct
- ✅ Extension API examples match current implementation
- ✅ Feature flag system documentation aligns with actual code
- ✅ Settings system documentation reflects current architecture
- ✅ Testing guides reference correct test file locations
- ✅ Vue component conventions match actual patterns
- ✅ TypeScript conventions align with codebase standards
## Review Notes
This audit was triggered by a codebase restructuring where
settings-related files were moved from `src/constants/` and
`src/stores/` to a new platform-based organization under
`src/platform/settings/`. Additionally, some service files were renamed
to follow the `use*` composable naming convention.
All documentation now accurately reflects the current codebase
structure. No functionality changes were made - only documentation
updates to maintain accuracy.
### Files with Intentionally Deleted References (No Fix Needed)
- `build/plugins/generateImportMapPlugin.ts` - Documented as removed in
ADR-0005, reference is intentionally showing historical context
## Testing
- Verified all updated file paths exist in the codebase
- Confirmed all code examples still compile with updated import paths
- Validated no broken internal documentation links
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10029-docs-Weekly-Documentation-Update-3256d73d36508177b363d4160085bbe9)
by [Unito](https://www.unito.io)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-03-16 07:13:32 -07:00
716 changed files with 47735 additions and 10388 deletions
These are the primary architectural guardrails. Every entity/litegraph change must be checked against them.
### Command Pattern (ADR 0003)
All entity state mutations MUST be expressible as **serializable, idempotent, deterministic commands**. This is required for CRDT sync, undo/redo, cross-environment portability, and gateway backends.
Flag:
- **Direct spatial mutation** — `node.pos = ...`, `node.size = ...`, `group.pos = ...` outside of a store or command. All spatial data flows through `layoutStore` commands.
- **Imperative fire-and-forget mutation** — Any new API that mutates entity state as a side effect rather than producing a serializable command object. Systems should produce command batches, not execute mutations directly.
- **Void-returning mutation APIs** — New entity mutation functions that return `void` instead of a result type (`{ status: 'applied' | 'rejected' | 'no-op' }`). Commands need error/rejection semantics.
- **Auto-incrementing IDs in new entity code** — New entity creation using auto-increment counters without acknowledging the CRDT collision problem. Concurrent environments need globally unique, stable identifiers.
### ECS Architecture (ADR 0008)
The graph domain model is migrating to ECS. New code must not make the migration harder.
Flag:
- **God-object growth** — New methods/properties added to `LGraphNode` (~4k lines), `LGraphCanvas` (~9k lines), `LGraph` (~3k lines), or `Subgraph`. Extract to systems, stores, or composables instead.
- **Mixed data and behavior** — New component-like data structures that contain methods or back-references to parent entities. ECS components are plain data objects.
- **New circular entity dependencies** — New circular imports between `LGraph` ↔ `Subgraph`, `LGraphNode` ↔ `LGraphCanvas`, or similar entity classes.
- **Direct `graph._version++`** — Mutating the private version counter directly instead of through a public API. Extensions already depend on this side-channel; it must become a proper API.
### Centralized Registries and ECS-Style Access
All entity data access should move toward centralized query patterns, not instance property access.
Flag:
- **New instance method/property patterns** — Adding `node.someProperty` or `node.someMethod()` for data that should be a component in the World, queried via `world.getComponent(entityId, ComponentType)`.
- **OOP inheritance for entity modeling** — Extending entity classes with new subclasses instead of composing behavior through components and systems.
- **Scattered state** — New entity state stored in multiple locations (class properties, stores, local variables) instead of being consolidated in the World or in a single store.
### Extension Ecosystem Impact
Entity API changes affect 40+ custom node repos. Changes to these patterns require an extension migration path.
For all other ADRs, iterate through each file in `docs/adr/` and extract the core lesson. Ensure changed code does not contradict accepted ADRs. Flag contradictions with proposed ADRs as directional guidance.
### How to Apply
1. Read `docs/adr/README.md` to get the full ADR index
2. For each ADR, read the Decision and Consequences sections
3. Check the diff against each ADR's constraints
4. Only flag ACTUAL violations in changed code, not pre-existing patterns
### Skip List
These ADRs can be skipped for most reviews (they cover completed or narrow-scope decisions):
- **ADR 0004** (Rejected — Fork PrimeVue) — only relevant if someone proposes forking PrimeVue again
## How to Check
1. Identify changed files in the entity/litegraph layer: `src/lib/litegraph/`, `src/ecs/`, `src/platform/`, entity-related stores
2. For Priority 1 patterns, use targeted searches:
2.**Imperative fire-and-forget APIs** — Functions that mutate entity state as side effects rather than producing serializable command objects. Systems should produce command batches, not execute mutations directly.
4.**Auto-increment IDs** — New entity creation via counters without addressing CRDT collision. Concurrent environments need globally unique identifiers.
5.**Missing transaction semantics** — Multi-entity operations without atomic grouping (e.g., node removal = 10+ deletes with no rollback on failure)
### Check B: ECS Architecture (ADR 0008)
Flag:
1.**God-object growth** — New methods/properties on `LGraphNode`, `LGraphCanvas`, `LGraph`, `Subgraph`
2.**Mixed data/behavior** — Component-like structures with methods or back-references
3.**OOP instance patterns** — New `node.someProperty` or `node.someMethod()` for data that should be a World component
4.**OOP inheritance** — New entity subclasses instead of component composition
- Priority 1 findings: N (command-pattern: N, ECS: N, ecosystem: N)
- Priority 2 findings: N
### Priority 1: Command Pattern & ECS
(List each with ADR reference, file, line, description)
### Priority 1: Extension Ecosystem Impact
(List each changed callback/API with affected custom node repos)
### Priority 2: General ADR Compliance
(List each with ADR reference, file, line, description)
### Compliant Patterns
(Note changes that positively align with ADR direction)
```
## Severity
- **Must fix**: Contradicts accepted ADR, or introduces imperative mutation API without command-pattern wrapper, or breaks extension callback without migration path
- **Should discuss**: Contradicts proposed ADR direction — either align or propose ADR amendment
- **Note**: Surfaces open architectural question not yet addressed by ADRs
**NEVER merge a backport PR without all CI checks passing.** This applies to both automation-created and manual cherry-pick PRs.
- **Automation PRs:** The `pr-backport.yaml` workflow now enables `gh pr merge --auto --squash`, so clean PRs auto-merge once CI passes. Monitor with polling (`gh pr list --base TARGET_BRANCH --state open`). Do not intervene unless CI fails.
- **Manual cherry-pick PRs:** After `gh pr create`, wait for CI before merging. Poll with `gh pr checks $PR --watch` or use a sleep+check loop. Only merge after all checks pass.
- **CI failures:** DO NOT use `--admin` to bypass failing CI. Analyze the failure, present it to the user with possible causes (test backported without implementation, missing dependency, flaky test), and let the user decide the next step.
If typecheck fails, stop and investigate before continuing. A broken branch after wave N means all subsequent waves will compound the problem.
If typecheck or tests fail, stop and investigate before continuing. A broken branch after wave N means all subsequent waves will compound the problem.
### Never Admin-Merge Without CI
In a previous bulk session, all 69 backport PRs were merged with `gh pr merge --squash --admin`, bypassing required CI checks. This shipped 3 test failures to a release branch. **Lesson: `--admin` skips all branch protection, including required status checks.** Only use `--admin` after confirming CI has passed (e.g., `gh pr checks $PR` shows all green), or rely on auto-merge (`--auto --squash`) which waits for CI by design.
# Check which got auto-PRs (auto-merge is enabled, so clean ones will self-merge after CI)
gh pr list --base TARGET_BRANCH --state open --limit 50 --json number,title
```
## Step 2: Review & Merge Clean Auto-PRs
> **Note:** The `pr-backport.yaml` workflow now enables `gh pr merge --auto --squash` on automation-created PRs. Clean PRs will auto-merge once CI passes — no manual merge needed for those.
## Step 2: Wait for CI & Merge Clean Auto-PRs
Most automation PRs will auto-merge once CI passes (via `--auto --squash` in the workflow). Monitor and handle failures:
7.**appModeStore.ts, painter files, GLSLShader files** don't exist on core/1.40 — `git rm` these
8.**Always validate JSON** after resolving locale file conflicts
9.**Dep refresh PRs** — skip on stable branches. Risk of transitive dep regressions outweighs audit cleanup. Cherry-pick individual CVE fixes instead.
10.**Verify after each wave** — run `pnpm typecheck` on the target branch after merging a batch. Catching breakage early prevents compounding errors.
10.**Verify after each wave** — run `pnpm typecheck && pnpm test:unit` on the target branch after merging a batch. Catching breakage early prevents compounding errors.
11.**Cloud-only PRs don't belong on core/\* branches** — app mode, cloud auth, and cloud-specific UI changes are irrelevant to local users. Always check PR scope against branch scope before backporting.
12.**Never admin-merge without CI** — `--admin` bypasses all branch protections including required status checks. A bulk session of 69 admin-merges shipped 3 test failures. Always wait for CI to pass first, or use `--auto --squash` which waits by design.
## CI Failure Triage
When CI fails on a backport PR, present failures to the user using this template:
```markdown
### PR #XXXX — CI Failed
- **Failing check:** test / lint / typecheck
- **Error:** (summary of the failure message)
- **Likely cause:** test backported without implementation / missing dependency / flaky test / snapshot mismatch
- **Recommendation:** backport PR #YYYY first / skip this PR / rerun CI after fixing prerequisites
description: 'Detect DOM elements where CSS contain:layout+style would improve rendering performance. Runs a Playwright-based audit on a large workflow, scores candidates by subtree size and sizing constraints, measures performance impact, and generates a ranked report.'
---
# CSS Containment Audit
Automatically finds DOM elements where adding `contain: layout style` would reduce browser recalculation overhead.
## What It Does
1. Loads a large workflow (245 nodes) in a real browser
2. Walks the DOM tree and scores every element as a containment candidate
3. For each high-scoring candidate, applies `contain: layout style` via JavaScript
4. Measures rendering performance (style recalcs, layouts, task duration) before and after
5. Takes before/after screenshots to detect visual breakage
6. Generates a ranked report with actionable recommendations
## When to Use
- After adding new Vue components to the node rendering pipeline
- When investigating rendering performance on large workflows
- Before and after refactoring node DOM structure
- As part of periodic performance audits
## How to Run
```bash
# Start the dev server first
pnpm dev &
# Run the audit (uses the @audit tag, not included in normal CI runs)
pnpm exec playwright test browser_tests/tests/containAudit.spec.ts --project=audit
This PR added `contain-layout contain-style` to the node inner wrapper div in `LGraphNode.vue`. The audit tool would have flagged this element as a high-scoring candidate because:
- **Externally constrained size** (`w-(--node-width)`, `flex-1` — dimensions set by CSS variables and flex parent)
- **Natural isolation boundary** between frequently-changing content (widgets) and infrequently-changing overlays (selection outlines, borders)
The actual change was a single line: adding `'contain-layout contain-style'` to the inner wrapper's class list at `src/renderer/extensions/vueNodes/components/LGraphNode.vue:79`.
description: 'Detect violations of the layered architecture import rules (base -> platform -> workbench -> renderer). Runs ESLint with the import-x/no-restricted-paths rule and generates a grouped report.'
---
# Layer Architecture Audit
Finds imports that violate the layered architecture boundary rules enforced by `import-x/no-restricted-paths` in `eslint.config.ts`.
## Layer Hierarchy (bottom to top)
```
renderer (top -- can import from all lower layers)
^
workbench
^
platform
^
base (bottom -- cannot import from any upper layer)
```
Each layer may only import from layers below it.
## How to Run
```bash
# Run ESLint filtering for just the layer boundary rule violations
pnpm lint 2>&1| grep 'import-x/no-restricted-paths' -B1 | head -200
```
To get a full structured report, run:
```bash
# Collect all violations from base/, platform/, workbench/ layers
| base -> platform | base/ importing from platform/ |
| base -> workbench | base/ importing from workbench/ |
| base -> renderer | base/ importing from renderer/ |
| platform -> workbench | platform/ importing from workbench/ |
| platform -> renderer | platform/ importing from renderer/ |
| workbench -> renderer | workbench/ importing from renderer/ |
## When to Use
- Before creating a PR that adds imports between `src/base/`, `src/platform/`, `src/workbench/`, or `src/renderer/`
- When auditing the codebase to find and plan migration of existing violations
- After moving files between layers to verify no new violations were introduced
## Fixing Violations
Common strategies to resolve a layer violation:
1.**Move the import target down** -- if the imported module doesn't depend on upper-layer concepts, move it to a lower layer
2.**Introduce an interface** -- define an interface/type in the lower layer and implement it in the upper layer via dependency injection or a registration pattern
3.**Move the importing file up** -- if the file logically belongs in a higher layer, relocate it
4.**Extract shared logic** -- pull the shared functionality into `base/` or a shared utility
3. The PR description includes a concrete, non-placeholder explanation of why an end-to-end regression test was not added.
Fail otherwise. When failing, mention which bug-fix signal you found and ask the author to either add or update a Playwright regression test under `browser_tests/` or add a concrete explanation in the PR description of why an end-to-end regression test is not practical.
- name:ADR compliance for entity/litegraph changes
mode:warning
instructions:|
Use only PR metadata already available in the review context: the changed-file list relative to the PR base, the PR description, and the diff content. Do not rely on shell commands.
This check applies ONLY when the PR modifies files under `src/lib/litegraph/`, `src/ecs/`, or files related to graph entities (nodes, links, widgets, slots, reroutes, groups, subgraphs).
If none of those paths appear in the changed files, pass immediately.
When applicable, check for:
1. **Command pattern (ADR 0003)**: Entity state mutations must be serializable, idempotent, deterministic commands — not imperative fire-and-forget side effects. Flag direct spatial mutation (`node.pos =`, `node.size =`, `group.pos =`) outside of a store or command, and any new void-returning mutation API that should produce a command object.
2. **God-object growth (ADR 0008)**: New methods/properties added to `LGraphNode`, `LGraphCanvas`, `LGraph`, or `Subgraph` that add responsibilities rather than extracting/migrating existing ones.
3. **ECS data/behavior separation (ADR 0008)**: Component-like data structures that contain methods or back-references to parent entities. ECS components must be plain data. New OOP instance patterns (`node.someProperty`, `node.someMethod()`) for data that should be a World component.
4. **Extension ecosystem (ADR 0008)**: Changes to extension-facing callbacks (`onConnectionsChange`, `onRemoved`, `onAdded`, `onConfigure`, `onConnectInput/Output`, `onWidgetChanged`), `node.widgets` access, `node.serialize` overrides, or `graph._version++` without migration guidance. These affect 40+ custom node repos.
Pass if none of these patterns are found in the diff.
When warning, reference the specific ADR by number and link to `docs/adr/` for context. Frame findings as directional guidance since ADR 0003 and 0008 are in Proposed status.
if [ "${{ needs.resolve-version.outputs.needs_release }}" = "true" ]; then
RELEASE_NOTE="⚠️ **Release PR must be merged first** - check [release workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)"
BODY=$''"${RELEASE_NOTE}"$'\n\n'"${BODY}"
fi
PYPI_NOTE="✅ **PyPI package confirmed available** — \`comfyui-frontend-package==${{ needs.resolve-version.outputs.target_version }}\` has been published and verified."
BODY=$''"${PYPI_NOTE}"$'\n\n'"${BODY}"
# Save to file for later use
printf '%s\n' "$BODY" > pr-body.txt
@@ -307,7 +427,11 @@ jobs:
fi
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
echo "PR already exists (#${EXISTING_PR}), updating branch will update the PR"
@@ -208,7 +208,7 @@ See @docs/testing/\*.md for detailed patterns.
3. Keep your module mocks contained
Do not use global mutable state within the test file
Use `vi.hoisted()` if necessary to allow for per-test Arrange phase manipulation of deeper mock state
4. For Component testing, use [Vue Test Utils](https://test-utils.vuejs.org/) and especially follow the advice [about making components easy to test](https://test-utils.vuejs.org/guide/essentials/easy-to-test.html)
4. For Component testing, prefer [@testing-library/vue](https://testing-library.com/docs/vue-testing-library/intro/) with `@testing-library/user-event` for user-centric, behavioral tests. [Vue Test Utils](https://test-utils.vuejs.org/) is also accepted, especially for tests that need direct access to the component wrapper (e.g., `findComponent`, `emitted()`). Follow the advice [about making components easy to test](https://test-utils.vuejs.org/guide/essentials/easy-to-test.html)
5. Aim for behavioral coverage of critical and new features
### Playwright / Browser / E2E Tests
@@ -231,6 +231,18 @@ See @docs/testing/\*.md for detailed patterns.
- Nx: <https://nx.dev/docs/reference/nx-commands>
- [Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
## Architecture Decision Records
All architectural decisions are documented in `docs/adr/`. Code changes must be consistent with accepted ADRs. Proposed ADRs indicate design direction and should be treated as guidance. See `.agents/checks/adr-compliance.md` for automated validation rules.
1. **Command pattern for all mutations**: Every entity state change must be a serializable, idempotent, deterministic command — replayable, undoable, and transmittable over CRDT. No imperative fire-and-forget mutation APIs. Systems produce command batches, not direct side effects.
2. **Centralized registries and ECS-style access**: Entity data lives in the World (centralized registry), queried via `world.getComponent(entityId, ComponentType)`. Do not add new instance properties/methods to entity classes. Do not use OOP inheritance for entity modeling.
3. **No god-object growth**: Do not add methods to `LGraphNode`, `LGraphCanvas`, `LGraph`, or `Subgraph`. Extract to systems, stores, or composables.
4. **Plain data components**: ECS components are plain data objects — no methods, no back-references to parent entities. Behavior belongs in systems (pure functions).
5. **Extension ecosystem impact**: Changes to entity callbacks (`onConnectionsChange`, `onRemoved`, `onAdded`, `onConnectInput/Output`, `onConfigure`, `onWidgetChanged`), `node.widgets` access, `node.serialize`, or `graph._version++` affect 40+ custom node repos and require migration guidance.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.