## Summary
Fixes two visual bugs in the Desktop app at small window sizes: the
search bar getting pushed/clipped in modal headers, and autocomplete
suggestion dropdowns being cut off by `overflow-hidden` ancestors.
## Changes
- **`SearchAutocomplete.vue`**: Wrap `ComboboxContent` in
`ComboboxPortal` so the suggestions dropdown teleports to `<body>`,
escaping `overflow-hidden` ancestors (fixes z-index clipping in Manager
dialog and other modals using `BaseModalLayout`)
- **`BaseModalLayout.vue`**: Replace `shrink-0` with `min-w-0` on the
header content container so the search bar can shrink at narrow window
sizes instead of overflowing and being clipped by the modal root's
`overflow-hidden`
- **`GraphCanvas.vue`**: Fix dead code where the native drag
(`app-drag`) div was nested inside a `v-if="workflowTabsPosition ===
'Topbar'"` block with its own mutually exclusive condition — move it
before the block and add `pointer-events-auto` so Desktop window
dragging works when tabs are in Sidebar position
## Why no E2E tests
- **`SearchAutocomplete` portal**: The fix is structural (teleport to
`<body>`). A meaningful regression test would require opening the
Manager dialog with a real or mocked extension list — that is a
substantial standalone effort tracked in #11714.
- **`BaseModalLayout` header shrink**: A viewport-resize assertion would
test CSS layout behaviour, not application logic; it would be fragile
and low-value.
- **`GraphCanvas` app-drag**: Desktop/Electron-only.
`-webkit-app-region: drag` cannot be exercised in headless Chromium.
Unit tests for `SearchAutocomplete` cover the new code paths (portal
rendering, suggestion display, item selection).
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk UI-only changes: adjusts layout CSS and combobox rendering
via `ComboboxPortal`, plus adds unit tests; no business logic or data
flow changes.
>
> **Overview**
> Fixes small-window Desktop UI issues where modal-header search inputs
could be clipped and autocomplete dropdowns could be cut off by
`overflow-hidden` ancestors.
>
> `SearchAutocomplete` now renders its suggestions list inside a
`ComboboxPortal` (teleporting the popper content outside clipping
containers) and adds a focused unit test suite covering empty/non-empty
suggestions, selection behavior, and `optionLabel` handling.
>
> `BaseModalLayout` tweaks header flexbox constraints (`min-w-0` on the
header content container) to allow the search bar to shrink instead of
overflowing.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
fd32d960f9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
## Summary
Add unit tests for three small mask editor Vue components (`SidePanel`,
`MaskEditorButton`, `BrushCursor`), all reaching **100%** across
coverage dimensions.
## Changes
- **What**: Add 3 sibling test files (17 tests total):
- `SidePanel.test.ts` (3): renders both `SettingsPanelContainer` and
`ImageLayerSettingsPanel`; forwards `toolManager` prop through to
`ImageLayerSettingsPanel`; works with the prop omitted.
- `MaskEditorButton.test.ts` (3): renders with localized `aria-label`
when `isSingleImageNode` is true; hidden via `v-show` (style `display:
none`) when false; click executes `Comfy.MaskEditor.OpenMaskEditor`
command.
- `BrushCursor.test.ts` (11): opacity 1 / 0 from `brushVisible`; size =
2 × effective brush size × zoom (with hardness scaling); border-radius
50% / 0% for Arc / Rect; position from `cursorPoint + panOffset -
radius`, with optional `containerRef.getBoundingClientRect` offset;
gradient preview hidden / shown; flat fill at `effectiveHardness === 1`.
## Review Focus
- `SidePanel` test stubs both child panels to bare `<div data-testid>`
so the test verifies wiring (prop forwarding, child rendering) without
being affected by the children's i18n / store dependencies.
- `MaskEditorButton` mocks `useCommandStore.execute` and
`useSelectionState.isSingleImageNode` (as a Vue ref). Real i18n via
`createI18n` + `globalInjection: true` — the template uses `$t(...)`
which requires global injection in composition mode.
- For the v-show hidden case, `getByLabelText('...', { selector:
'button' })` works where `getByRole('button', { hidden: true })` doesn't
reliably resolve through the `<Button>` wrapper component.
- `BrushCursor` uses `reactive()` mock store and queries the brush /
gradient elements by id (`#maskEditor_brush`,
`#maskEditor_brushPreviewGradient`). File-level eslint-disable for
`testing-library/no-node-access` because the component's anchor elements
are styled divs without ARIA roles or labels.
- The `radial-gradient` (hardness < 1) branch of `gradientBackground` is
intentionally **not** asserted via rendered DOM: the computed returns a
multi-line template literal that happy-dom's CSS parser drops entirely.
The math (`getEffectiveBrushSize` / `getEffectiveHardness`) is covered
by `brushUtils.test.ts`. v8 reports 100% branch coverage because Vue
evaluates the computed regardless of whether the resulting style string
is parsed by the DOM.
- Style aligned with sibling tests: `should ...` naming, `describe`
grouped by feature, `vi.hoisted` for command-store / selection-state
mocks, real i18n via `createI18n`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11648-test-add-unit-tests-for-SidePanel-MaskEditorButton-and-BrushCursor-34e6d73d365081e38c4afee32ddf2b0b)
by [Unito](https://www.unito.io)
## Summary
Adds 22 unit tests across 3 files covering WidgetDOM, MultiSelectWidget,
and TextPreviewWidget. Part of a widget-test-coverage sequence.
## Changes
- **What**:
- \`WidgetDOM.test.ts\` (4) — mounts the resolved DOMWidget element into
the container, empty container when no host node resolves, skips mount
when resolved widget is not a DOM widget, visible root for pointer-event
capture.
- \`MultiSelectWidget.test.ts\` (8) — forwards \`inputSpec.options\`,
falls back to empty options, placeholder from
\`multi_select.placeholder\`, default placeholder, chip vs comma
display, initial selection forwarding.
- \`TextPreviewWidget.test.ts\` (10) — plain text, newline→\`<br>\`,
bare-URL auto-linking, \`[[label|url]]\` http link with target/rel
safety, non-http falls back to escaped label (XSS-safe), skeleton
visibility transitions via mocked executionStore.
## Review Focus
- \`WidgetDOM\` mocks \`useCanvasStore\`, \`resolveWidgetFromHostNode\`,
and \`isDOMWidget\` at the module boundary; test asserts identity of the
mounted element (same \`HTMLElement\` reference) rather than
canvas-side-effects.
- \`TextPreviewWidget\` replaces \`useExecutionStore\` with a
\`reactive()\` proxy held in a hoisted holder so watcher assertions see
real reactive mutations (plain \`vi.hoisted\` objects don't trigger Vue
effects).
- No changes to any source component.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11445-test-add-unit-tests-for-graph-level-widgets-3486d73d3650816180d5f31a523f5c22)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Harden the `ChangeTracker` lifecycle to eliminate the class of bugs
where an inactive workflow's tracker silently captures the wrong graph
state. Renames `checkState()` to `captureCanvasState()` with a
self-defending assertion, introduces `deactivate()` and
`prepareForSave()` lifecycle methods, and closes a latent undo-history
corruption bug discovered during code review.
## Background
ComfyUI supports multiple workflows open as tabs, but only one canvas
(`app.rootGraph`) exists at a time. When the user switches tabs, the old
workflow's graph is unloaded and the new one is loaded into this shared
canvas.
The old `checkState()` method serialized `app.rootGraph` into
`activeState` to track changes for undo/redo. It had no awareness of
*which* workflow it belonged to -- if called on an inactive tab's
tracker, it would capture the active tab's graph data and silently
overwrite the inactive workflow's state. This caused permanent data loss
(fixed in PR #10745 with caller-side `isActive` guards).
The caller-side guards were fragile: every new call site had to remember
to add the guard, and forgetting would reintroduce the same silent data
corruption. Additionally, `beforeLoadNewGraph` only called `store()`
(viewport/outputs) without `checkState()`, meaning canvas state could be
stale if a tab switch happened without a preceding mouseup event.
### Before (fragile)
```
saveWorkflow(workflow):
if (isActive(workflow)) <-- caller must remember this guard
workflow.changeTracker.checkState() <-- name implies "read", actually writes
...
beforeLoadNewGraph():
activeWorkflow.changeTracker.store() <-- only saves viewport, NOT graph state
```
### After (self-defending)
```
saveWorkflow(workflow):
workflow.changeTracker.prepareForSave() <-- handles active/inactive internally
...
beforeLoadNewGraph():
activeWorkflow.changeTracker.deactivate() <-- captures graph + viewport together
```
## Changes
- Rename `checkState` to `captureCanvasState` with active-tracker
assertion
- Add `deactivate()` and `prepareForSave()` lifecycle methods
- Fix undo-history corruption: `captureCanvasState()` guarded by
`_restoringState`
- Fix viewport regression during undo: `deactivate()` skips
`captureCanvasState()` during undo/redo but always calls `store()` to
preserve viewport (regression from PR #10247)
- Log inactive tracker warnings unconditionally at warn level (not
DEV-only)
- Deprecated `checkState()` wrapper for extension compatibility
- Rename `checkState` to `captureCanvasState` in
`useWidgetSelectActions` composable
- Add `appModeStore.ts` to manual call sites documentation
- Add `checkState()` deprecation note to architecture docs
- Add 16 unit tests covering all guard conditions, lifecycle methods,
and undo behavior
- Add E2E test: "Undo preserves viewport offset"
## New ChangeTracker Public API
| Method | Caller | Purpose |
|--------|--------|---------|
| `captureCanvasState()` | Event handlers, UI interactions | Snapshots
canvas into activeState, pushes undo. Asserts active tracker. |
| `deactivate()` | `beforeLoadNewGraph` only | `captureCanvasState()`
(skipped during undo/redo) + `store()`. Freezes state for tab switch. |
| `prepareForSave()` | Save paths only | Active: `captureCanvasState()`.
Inactive: no-op. |
| `checkState()` | **Deprecated** -- extensions only | Wrapper that
delegates to `captureCanvasState()` with deprecation warning. |
| `store()` | Internal to `deactivate()` | Saves viewport, outputs,
subgraph navigation. |
| `restore()` | `afterLoadNewGraph` | Restores viewport, outputs,
subgraph navigation. |
| `reset()` | `afterLoadNewGraph`, save | Resets initial state (marks as
"clean"). |
## Test plan
- [x] Unit tests: 16 tests covering all guard conditions, state capture,
undo queue behavior
- [x] E2E test: "Undo preserves viewport offset" verifies no viewport
drift on undo
- [x] E2E test: "Prevents captureCanvasState from corrupting workflow
state during tab switch"
- [x] Existing E2E: "Closing an inactive tab with save preserves its own
content"
- [ ] Manual: rapidly switch tabs during undo/redo, verify no viewport
drift
- [ ] Manual: verify extensions calling `checkState()` see deprecation
warning in console
## Summary
- Adds `selectionToolboxMoreActions.spec.ts` with E2E coverage for
previously untested toolbox actions
- Covers: pin/unpin, minimize/expand, adjust size, copy, duplicate,
refresh button, align (top/left), distribute (horizontal/vertical),
alignment options hidden for single selection, multi-node bypass toggle
- Part of the FixIt Burndown test coverage initiative (toolbox actions)
## Test plan
- [ ] All new tests pass in CI
- [ ] No regressions in existing selectionToolbox tests
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10968-test-expand-E2E-coverage-for-toolbox-actions-33c6d73d3650811286cefdd0eb4f5242)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Problem
When a node with DOM widget overlays (e.g. CLIPTextEncode) is collapsed,
the overlay elements can intercept pointer events intended for the
canvas collapse toggler, making click-to-expand unreliable.
## Root Cause
`updateWidgets()` runs during `onDrawForeground` (canvas render cycle)
and sets `widgetState.visible = false` for collapsed nodes. `v-show`
then hides the element with `display: none`. However, there is a timing
gap between the canvas state change and Vue's DOM update — during this
gap the widget overlay still intercepts pointer events.
## Fix
Add `!widgetState.visible` to the `pointerEvents` condition in
`composeStyle()`. This immediately sets `pointer-events: none` when the
widget becomes invisible, preventing event interception before `v-show`
applies `display: none`.
Also restores click-to-expand in the E2E test, removing the programmatic
`node.collapse()` workaround from PR #10967.
- Fixes#11006
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11063-fix-disable-pointer-events-on-non-visible-DOM-widget-overlays-33e6d73d36508179a83cd47121cf933f)
by [Unito](https://www.unito.io)
## Summary
The node "More Options" (⋮) context menu had `md:max-h-none
md:overflow-y-visible` responsive overrides that removed the height
constraint and scrollability on desktop (768px+). When the menu had many
items (e.g., KSampler), items below the viewport fold were inaccessible
with no scrollbar.
Removed the desktop overrides so `max-h-[80vh] overflow-y-auto` applies
at all screen sizes.
- Fixes#10824
## Red-Green Verification
| Commit | CI Status | Purpose |
|--------|-----------|---------|
| `test: add failing test for node context menu viewport overflow` |
🔴 Red | Proves the test catches the bug |
| `fix: prevent node context menu from overflowing viewport on desktop`
| 🟢 Green | Proves the fix resolves the bug |
## Test Plan
- [ ] CI red on test-only commit
- [ ] CI green on fix commit
- [ ] Manual verification: zoom out to 50%, open node More Options menu,
verify last item ("Remove") is scrollable
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10854-fix-prevent-node-context-menu-from-overflowing-viewport-on-desktop-3396d73d365081989403c981847aeda6)
by [Unito](https://www.unito.io)
Adds `browser_tests/tests/canvasModeSelector.spec.ts`, covering the
canvas toolbar mode-selector component that was introduced with no E2E
coverage.
Covers:
- Trigger button: toolbar visibility, `aria-expanded` state, icon
reflects active mode
- Popover lifecycle: open on click, close on re-click / item selection /
Escape
- Mode switching: clicking Hand/Select drives `canvas.state.readOnly`;
clicking the active item is a no-op
- ARIA state: `aria-checked` and roving `tabindex` track active mode,
including state driven by external commands
- Keyboard navigation: ArrowDown/Up with wraparound, Escape restores
focus to trigger — all using `toBeFocused()` retrying assertions
- Focus management: popover auto-focuses the checked item on open
- Keybinding integration: `H` / `V` keys update both
`canvas.state.readOnly` and the trigger icon
- Shortcut hint display: both menu items render non-empty key-sequence
hints
22 tests across 7 `describe` groups. All selectors are ARIA-driven.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10934-test-add-E2E-tests-for-CanvasModeSelector-toolbar-component-33b6d73d3650819cb2cfdca22bf0b9a5)
by [Unito](https://www.unito.io)
## Summary
- Replace raw CSS selectors (`.lg-node-header`, `.p-contextmenu`,
`.node-title-editor input`, `.image-preview img`) with centralized
`TestIds` constants and existing fixtures in the context menu E2E spec
- Add `data-testid="title-editor-input"` to TitleEditor overlay for
stable selector targeting
- Use `NodeLibrarySidebarTab` fixture for node library sidebar
interaction
## Changes
- `browser_tests/fixtures/selectors.ts`: add `pinIndicator`,
`innerWrapper`, `titleEditorInput`, `mainImage` to `TestIds.node`
- `browser_tests/fixtures/utils/vueNodeFixtures.ts`: add `pinIndicator`
getter
- `src/components/graph/TitleEditor.vue`: add `data-testid` via
`input-attrs`
- `browser_tests/.../contextMenu.spec.ts`: replace all raw selectors
with TestIds/fixtures
## Test plan
- [x] All 23 context menu E2E tests pass locally
- [x] Typecheck passes
- [x] Lint passes
Fixes#10750Fixes#10749
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10760-test-replace-raw-CSS-selectors-with-TestIds-in-context-menu-spec-3336d73d3650818790c0e32e0b6f1e98)
by [Unito](https://www.unito.io)
## Summary
- Convert `fromAny` → `fromPartial` in 7 test files where object
literals or interfaces are passed
- `fromPartial` type-checks the provided fields, unlike `fromAny` which
bypasses all checking (same as `as unknown as`)
- Class-based types (`LGraphNode`, `LGraph`) remain `fromAny` due to
shoehorn's `PartialDeep` incompatibility with class constructors
## Changes
- **Pure conversions** (all `fromAny` → `fromPartial`):
`domWidgetZIndex`, `matchPromotedInput`, `promotionUtils`,
`subgraphNavigationStore`
- **Mixed** (some converted, some kept): `promotedWidgetView`,
`widgetUtil`
- **Cleanup**: `nodeOutputStore` type param normalization
Follows up on #10761.
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm vitest run` on all 7 changed files — 169 tests pass
- [x] `pnpm lint` passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10788-test-migrate-fromAny-to-fromPartial-for-type-checked-test-mocks-3356d73d365081f7bf61d48a47af530c)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
- Replace all `as unknown as Type` assertions in 59 unit test files with
type-safe `@total-typescript/shoehorn` functions
- Use `fromPartial<Type>()` for partial mock objects where deep-partial
type-checks (21 files)
- Use `fromAny<Type>()` for fundamentally incompatible types: null,
undefined, primitives, variables, class expressions, and mocks with
test-specific extra properties that `PartialDeepObject` rejects
(remaining files)
- All explicit type parameters preserved so TypeScript return types are
correct
- Browser test `.spec.ts` files excluded (shoehorn unavailable in
`page.evaluate` browser context)
## Verification
- `pnpm typecheck` ✅
- `pnpm lint` ✅
- `pnpm format` ✅
- Pre-commit hooks passed (format + oxlint + eslint + typecheck)
- Migrated test files verified passing (ran representative subset)
- No test behavior changes — only type assertion syntax changed
- No UI changes — screenshots not applicable
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10761-test-migrate-as-unknown-as-to-total-typescript-shoehorn-3336d73d365081f6b8adc44db5dcc380)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
## What
Replace `v-if` with `v-show` on SelectionRectangle and NodeTooltip
components.
## Why
Firefox profiler shows 687 Vue `insert` markers from mount/unmount
cycling during canvas interaction. These components toggle frequently
during drag and mouse move events.
## How
- **SelectionRectangle**: `v-if` → `v-show` (single element, safe to
keep in DOM)
- **NodeTooltip**: `v-if` → `v-show` + no-op guard on `hideTooltip()` to
skip redundant reactivity triggers
## Perf Impact
Expected reduction: ~687 Vue insert/remove operations per profiling
session
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9401-fix-use-v-show-for-frequently-toggled-canvas-overlay-components-31a6d73d365081aba2d7fce079bde7e9)
by [Unito](https://www.unito.io)
## Summary
Phase 3 of the VTL migration: migrate 8 hard-case component tests from
@vue/test-utils to @testing-library/vue (68 tests).
Stacked on #10490.
## Changes
- **What**: Migrate SignInForm, CurrentUserButton, NodeSearchBoxPopover,
BaseThumbnail, JobAssetsList, SelectionToolbox, QueueOverlayExpanded,
PackVersionSelectorPopover from VTU to VTL
- **`wrapper.vm` elimination**: 13 instances across 4 files (5 in
SignInForm, 3 in CurrentUserButton, 3 in PackVersionSelectorPopover, 2
in BaseThumbnail) replaced with user interactions or removed
- **`vm.$emit()` on stubs**: Interactive stubs with `setup(_, { emit })`
expose buttons or closure-based emit functions (QueueOverlayExpanded,
NodeSearchBoxPopover, JobAssetsList)
- **Removed**: 6 change-detector/redundant tests, 3 `@ts-expect-error`
annotations, `PackVersionSelectorVM` interface, `getVM` helper
- **BaseThumbnail**: Removed `useEventListener` mock — real event
handler attaches, `fireEvent.error(img)` triggers error state
## Review Focus
- Interactive stub patterns: `JobAssetsListStub` and `NodeSearchBoxStub`
use closure-based emit functions to trigger parent event handlers
without `vm.$emit`
- SignInForm form submission test fills PrimeVue Form fields via
`userEvent.type` and submits via button click (replaces `vm.onSubmit()`
direct call)
- CurrentUserButton Popover stub tracks open/close state reactively
- JobAssetsList: file-level `eslint-disable` for
`no-container`/`no-node-access`/`prefer-user-event` since stubs lack
ARIA roles and hover tests need `fireEvent`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10493-test-migrate-8-hard-case-component-tests-from-VTU-to-VTL-Phase-3-32e6d73d365081f88097df634606d7e3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## 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)
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>
## 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
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
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
Reorder condition checks in `forwardPanEvent` so the cheap
`isMiddlePointerInput()` check runs first, avoiding expensive
`hasTextSelection()` → `window.getSelection().toString().trim()` on
every pointermove event.
## Changes
- **What**: `forwardPanEvent` now early-returns on
`!isMiddlePointerInput(e)` before calling `shouldIgnoreCopyPaste`, which
internally calls `hasTextSelection()`. Since most pointermove events are
not middle-button, this skips the expensive `toString()` call entirely.
## Review Focus
Semantic equivalence: the original condition was `(A && B) || C →
return`. Rewritten as two guards: `if (C) return; if (A && B) return;`.
The logic is equivalent — if `C` (`!isMiddlePointerInput`) is true, we
return regardless of `A && B`. If `C` is false (middle button), we check
`A && B` (`shouldIgnoreCopyPaste && activeElement`).
## Evidence
Backlog item #19. rizumu's Chrome DevTools profiling (Mar 2026) of a
245-node workflow showed `toString()` in `hasTextSelection` called on
every pointermove. Source: Slack `#C095BJSFV24` thread
`p1772823346206479`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9892-fix-reorder-forwardPanEvent-conditions-to-skip-expensive-hasTextSelection-on-non-middle--3226d73d365081d38b89c8bb1dde3693)
by [Unito](https://www.unito.io)
## Problem
When navigating back from a subgraph to the root graph, the SubgraphNode
can retain a stale progress bar. This happens because the progress
watcher in `GraphCanvas.vue` watches `[nodeLocationProgressStates,
canvasStore.canvas]`, but neither value changes reference during
subgraph navigation:
- `nodeLocationProgressStates` is already `{}` (execution completed
while viewing the subgraph)
- `canvasStore.canvas` is a `shallowRef` set once at startup — only
`canvas.graph` changes (via `setGraph()`)
**Reproduction** (from PR #4382 comment thread by @guill):
1. Create a subgraph with a KSampler
2. Execute the workflow
3. While progress bar is halfway, enter the subgraph
4. Wait for execution to complete
5. Navigate back to root graph
6. Progress bar is stuck at 50%
## Root Cause
`canvasStore.canvas` is a `shallowRef` — subgraph navigation mutates
`canvas.graph` (a nested property) via `LGraphCanvas.setGraph()`, which
doesn't trigger a shallow watch. The watcher never re-fires to clear
stale `node.progress` values.
## Fix
Add `canvasStore.currentGraph` to the watcher's dependency array. This
is already a `shallowRef` in `canvasStore` that's updated on every
`litegraph:set-graph` event. Zero overhead, precise targeting.
## Context
- Original discussion:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/4382/files/BASE..868e047272f6c5d710db7e607b8997d4c243490f#r2202024855
- PR #9248 correctly removed `deep: true` from this watcher but missed
the subgraph edge case
- `deep: true` was the wrong fix — `canvasStore.currentGraph` is the
precise solution
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9865-fix-clear-stale-progress-bar-on-SubgraphNode-after-navigation-3226d73d3650811c8f9de612d81ef98a)
by [Unito](https://www.unito.io)
## Summary
Automatically clears transient validation errors
(`value_bigger_than_max`, `value_smaller_than_min`, `value_not_in_list`,
`required_input_missing`) when the user modifies a widget value or
connects an input slot, so resolved errors don't linger in the error
panel. Also clears missing model state when the user changes a combo
widget value.
## Changes
- **`useNodeErrorAutoResolve` composable**: watches widget changes and
slot connections, clears matching errors via `executionErrorStore`
- **`executionErrorStore`**: adds `clearSimpleNodeErrors` and
`clearSimpleWidgetErrorIfValid` with granular per-slot error removal
- **`executionErrorUtil`**: adds `isValueStillOutOfRange` to prevent
premature clearing when a new value still violates the constraint
- **`graphTraversalUtil`**: adds `getExecutionIdFromNodeData` for
subgraph-aware execution ID resolution
- **`GraphCanvas.vue`**: fixes subgraph error key lookup by using
`getExecutionIdByNode` instead of raw `node.id`
- **`NodeWidgets.vue`**: wires up the new composable to the widget layer
- **`missingModelStore`**: adds `removeMissingModelByWidget` to clear
missing model state on widget value change
- **`useGraphNodeManager`**: registers composable per node
- **Tests**: 126 new unit tests covering error clearing, range
validation, and graph traversal edge cases
## Screenshots
https://github.com/user-attachments/assets/515ea811-ff84-482a-a866-a17e5c779c39https://github.com/user-attachments/assets/a2b30f02-4929-4537-952c-a0febe20f02e
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9464-feat-auto-resolve-simple-validation-errors-on-widget-change-and-slot-connection-31b6d73d3650816b8afdc34f4b40295a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All three watchers used { deep: true } unnecessarily because their
watched sources already produce new object/array references on change,
making deep traversal redundant:
- GraphView.vue: queueStore.tasks is a computed that spreads three
shallowRef arrays into a new array each time. TaskItemImpl instances
are immutable (readonly fields, replaced not mutated).
- DomWidget.vue: Replaced opaque widgetState deep watcher with explicit
property deps (pos, size, zIndex, readonly, positionOverride). During
60 FPS pan/zoom, Vue no longer walks the entire DomWidgetState object
graph including the markRaw widget — only 5 leaf properties are
checked. pos and size are new array literals each frame; the rest are
primitives.
- GraphCanvas.vue: nodeLocationProgressStates is a computed returning a
new Record on every execution progress event (nodeProgressStates is
replaced wholesale via WebSocket handler).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9248-perf-remove-deep-true-from-3-hot-watchers-to-reduce-reactivity-overhead-3136d73d365081278f18da5a2eef6971)
by [Unito](https://www.unito.io)
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
When many nodes are rendered in the transform container, both zoom and
pan can cause FPS drops because the browser re-rasterizes all visible
content at the new transform. `will-change: transform` tells the browser
to keep the layer as a GPU texture and skip re-rasterization during
active interaction, restoring visual quality only after settling.
- Add pointer drag detection so `will-change: transform` covers pan in
addition to zoom. Without this, dragging with 256+ nodes causes jank as
the browser re-rasterizes the entire layer on every frame of the pan.
- Fix settleDelay from 16ms to 256ms. At 16ms the debounce fires between
consecutive wheel events (~50ms apart on a physical mouse), causing
`will-change` to toggle on/off rapidly. Each toggle forces the browser
to promote/demote the compositor layer, which is more expensive than not
having the optimization at all.
- Replace scoped CSS with Tailwind `will-change-transform`.
- Remove per-node `will-change: transform` on `.lg-node`. Promoting each
node to its own compositor layer (256 nodes = 256 GPU textures)
increases memory pressure and compositing overhead, making performance
worse than a single promoted container.
- Previously, the virtual DOM of Nodes was updated during zooming and
dragging, but now this update is avoided through some techniques.
- Using the 3D versions of scale and translate can provide a smoother
experience when dealing with a large number of nodes.
## Test plan
- [x] Unit tests updated and passing
- [x] Manual: verify during both zoom and pan
- [x] Manual: compare pan FPS with 256 nodes before/after
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9649-perf-detect-pointer-drag-in-useTransformSettling-for-pan-optimization-31e6d73d3650818bb2c3ccd01a465140)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
- Fixes incorrect workflow loading after page refresh when the active
workflow is saved and unmodified
- Adds saved-workflow fallback in `loadPreviousWorkflowFromStorage()`
before falling back to latest draft
- Calls `openWorkflow()` in `restoreWorkflowTabsState()` to activate the
correct tab after restoration
- Fixes#9317
## Test plan
- [x] Unit tests for saved-workflow fallback (draft missing, saved
workflow exists)
- [x] Unit tests for correct tab activation after restoration
- [x] Unit tests for existing behavior preservation (draft preferred
over saved, no-session-path fallback)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All 117 persistence tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9318-fix-restore-correct-workflow-on-page-reload-3166d73d365081ba9139f7c23c917aa4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
The canvas mode selector popover (Select/Hand mode) uses plain `div`
elements for its menu items, making them completely inaccessible to
keyboard-only users and screen readers. This PR replaces them with
proper semantic HTML and ARIA attributes.
## Problem (AS-IS)
As reported in #9519, the canvas mode selector popover has the following
accessibility issues:
1. **Menu items are `div` elements** — they cannot receive keyboard
focus, so users cannot Tab into the popover or activate items with
Enter/Space. Keyboard-only users are completely locked out of switching
between Select and Hand (pan) mode via the popover.
2. **No ARIA roles** — screen readers announce the popover content as
generic text rather than an interactive menu. Users relying on assistive
technology have no way to understand that these are selectable options.
3. **No keyboard navigation within the popover** — even if a user
somehow focuses an item, there are no ArrowUp/ArrowDown handlers to move
between options, which is the standard interaction pattern for
`role="menu"` widgets (WAI-ARIA Menu Pattern).
4. **Decorative icons are not hidden from assistive technology** — icon
elements (`<i>` tags) are exposed to screen readers, adding noise to the
announcement.
5. **The bottom toolbar (`GraphCanvasMenu`) lacks semantic grouping** —
the `ButtonGroup` container has no `role="toolbar"` or `aria-label`, so
screen readers cannot convey that these buttons form a related control
group.
> Note: Pan mode itself already has keyboard shortcuts (`H` for
Hand/Lock, `V` for Select/Unlock), but the popover UI that surfaces
these options is not keyboard-accessible.
## Solution (TO-BE)
1. **Replace `div` → `button`** for menu items in `CanvasModeSelector` —
buttons are natively focusable and activatable via Enter/Space without
extra work.
2. **Add `role="menu"` on the container and `role="menuitem"` on each
option** — screen readers now announce "Canvas Mode menu" with two menu
items.
3. **Add ArrowUp/ArrowDown keyboard navigation** with wrap-around —
pressing ArrowDown on "Select" moves focus to "Hand", and vice versa.
This follows the WAI-ARIA Menu Pattern.
4. **Add `aria-label` to each menu item and `aria-hidden="true"` to
decorative icons** — screen readers announce "Select" / "Hand" clearly
without icon noise.
5. **Add `role="toolbar"` with `aria-label="Canvas Toolbar"` to the
`ButtonGroup`** — screen readers identify the bottom control bar as a
coherent toolbar.
## Changes
- **What**: Accessibility improvements to `CanvasModeSelector` popover
and `GraphCanvasMenu` toolbar
- **Dependencies**: None — only HTML/ARIA attribute changes and two new
i18n keys (`canvasMode`, `canvasToolbar`)
## Review Focus
- Verify the `button` elements render visually identical to the previous
`div` elements (same padding, hover styles, cursor)
- Confirm ArrowUp/ArrowDown navigation works correctly in the popover
- Check that screen readers announce the menu and toolbar correctly
Fixes#9519
> Note: The issue also requests Space-bar hold-to-pan, Tab through node
ports, and link creation mode keyboard shortcuts. These are significant
new features beyond the scope of this accessibility fix and should be
tracked separately.
## Test plan
- [x] Unit tests for ARIA roles, button elements, aria-labels,
aria-hidden, and arrow key navigation (7 tests)
- [ ] Manual: open canvas mode selector popover → Tab focuses first item
→ ArrowDown/ArrowUp navigates → Enter/Space selects
- [ ] Screen reader: verify "Canvas Mode menu" with "Select" and "Hand"
menu items are announced
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9526-fix-improve-canvas-menu-keyboard-navigation-and-ARIA-accessibility-31c6d73d3650814c9487ecf748cf6a99)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Enable `better-tailwindcss/enforce-consistent-class-order` lint rule and
auto-fix all 1027 violations across 263 files. Stacked on #9427.
## Changes
- **What**: Sort Tailwind classes into consistent order via `eslint
--fix`
- Enable `enforce-consistent-class-order` as `'error'` in eslint config
- Purely cosmetic reordering — no behavioral or visual changes
## Review Focus
Mechanical auto-fix PR — all changes are class reordering only. This is
the largest diff but lowest risk since it changes no class names, only
their order.
**Stack:** #9417 → #9427 → **this PR**
Fixes#9300 (partial — 3 of 3 rules)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9428-fix-enable-enforce-consistent-class-order-tailwind-lint-rule-31a6d73d3650811c9065f5178ba3e724)
by [Unito](https://www.unito.io)
## Summary
Enable `better-tailwindcss/enforce-canonical-classes` lint rule and
auto-fix all 611 violations across 173 files. Stacked on #9417.
## Changes
- **What**: Simplify Tailwind classes to canonical forms via `eslint
--fix`:
- `h-X w-X` → `size-X`
- `overflow-x-hidden overflow-y-hidden` → `overflow-hidden`
- and other canonical simplifications
- Enable `enforce-canonical-classes` as `'error'` in eslint config
## Review Focus
Mechanical auto-fix PR — all changes produced by `eslint --fix`. No
visual or behavioral changes; canonical forms are functionally
identical.
**Stack:** #9417 → **this PR** → PR 3 (class order)
Fixes#9300 (partial — 2 of 3 rules)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9427-fix-enable-enforce-canonical-classes-tailwind-lint-rule-31a6d73d365081a49340d7d4640ede45)
by [Unito](https://www.unito.io)
## Summary
Add workflow sharing by URL and a multi-step ComfyHub publish wizard,
gated by feature flags and an optional profile gate.
## Changes
- **What**: Share dialog with URL generation and asset warnings;
ComfyHub publish wizard (Describe → Examples → Finish) with thumbnail
upload and tags; profile gate flow; shared workflow URL loader with
confirmation dialog
- **Dependencies**: None (new `sharing/` module under
`src/platform/workflow/`)
## Review Focus
- Three new feature flags: `workflow_sharing_enabled`,
`comfyhub_upload_enabled`, `comfyhub_profile_gate_enabled`
- Share service API contract and stale-share detection
(`workflowShareService.ts`)
- Publish wizard and profile gate state management
- Shared workflow URL loading and query-param preservation
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8951-feat-share-workflow-by-URL-30b6d73d3650813ebbfafdad775bfb33)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Enable `better-tailwindcss/no-deprecated-classes` lint rule and auto-fix
all 103 violations across 65 files. First PR in a stacked series for
#9300.
## Changes
- **What**: Replace deprecated Tailwind v3 classes with v4 equivalents:
- `rounded` → `rounded-sm` (85)
- `flex-shrink-0` → `shrink-0` (16)
- `flex-grow` → `grow` (2)
- Enable `no-deprecated-classes` as `'error'` in eslint config
- Update one test asserting on `'rounded'` class string
## Review Focus
Mechanical auto-fix PR — all changes produced by `eslint --fix`. No
visual or behavioral changes (Tailwind v4 aliases these classes
identically).
Fixes#9300 (partial — 1 of 3 rules)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9417-fix-enable-no-deprecated-classes-tailwind-lint-rule-31a6d73d3650819eaef4cf8ad84fb186)
by [Unito](https://www.unito.io)
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Fix deterministic DOM widget clip-path rendering to resolve the flaky
"Can drag node" screenshot test.
## Root Cause
`useDomClipping.updateClipPath()` schedules clip-path calculation in a
`requestAnimationFrame`, but `DomWidget.vue`'s watcher reads
`clippingStyle.value` synchronously before the RAF fires. The stale
clip-path gets baked into `style.value` and never updated when the RAF
completes, causing the textarea DOM widget to non-deterministically
render in front of or behind the canvas-drawn node selection border.
## Fix
- Extract `composeStyle()` function and add a dedicated watcher on
`clippingStyle` that recomposes the final inline style whenever the
RAF-deferred clip-path updates
- Add `enableDomClipping` to the main watcher dependency array so
toggling the clipping setting immediately recomposes the style
- Add `moveMouseToEmptyArea()` call in the test as a secondary
stabilizer against hover highlight non-determinism
- Delete stale snapshot so CI regenerates it with correct clip-path
behavior
- Fixes#4658
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Add persistent upgrade CTAs for free-tier users: a topbar button and
"Upgrade to add credits" replacing "Add Credits" in popovers and
settings panels.
## Changes
- **What**:
- New `TopbarSubscribeButton` component in both GraphCanvas and
LinearView topbars, visible only to free-tier users
- Profile popover (legacy + workspace): free-tier users see "Upgrade to
add credits" instead of "Add Credits", linking directly to the pricing
table
- Manage Plan settings (legacy + workspace): same replacement —
free-tier users see "Upgrade to add credits" instead of "Add Credits"
- Paid-tier users retain the original "Add Credits" behavior in all
locations
- All upgrade buttons go directly to the pricing table (one-step flow)
## Review Focus
- The `isFreeTier` conditional gating on the buttons — ensure free-tier
users see upgrade CTAs and paid users see normal Add Credits
- Layout in Manage Plan panels uses `flex flex-col gap-3` to stack the
upgrade button below the usage history link instead of side-by-side
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9315-feat-add-ever-present-upgrade-button-for-free-tier-users-3166d73d365081228cdfe6a67fec6aec)
by [Unito](https://www.unito.io)
## Summary
Fix multiple issues with promoted widget resolution in nested subgraphs,
ensuring correct value propagation, slot matching, and rendering for
deeply nested promoted widgets.
## Changes
- **What**: Stabilize nested subgraph promoted widget resolution chain
- Use deep source keys for promoted widget values in Vue rendering mode
- Resolve effective widget options from the source widget instead of the
promoted view
- Stabilize slot resolution for nested promoted widgets
- Preserve combo value rendering for promoted subgraph widgets
- Prevent subgraph definition deletion while other nodes still reference
the same type
- Clean up unused exported resolution types
## Review Focus
- `resolveConcretePromotedWidget.ts` — new recursive resolution logic
for deeply nested promoted widgets
- `useGraphNodeManager.ts` — option extraction now uses
`effectiveWidget` for promoted widgets
- `SubgraphNode.ts` — unpack no longer force-deletes definitions
referenced by other nodes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9282-fix-stabilize-nested-subgraph-promoted-widget-resolution-3146d73d365081208a4fe931bb7569cf)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
- fix sizing of sidebars in app mode
- update feedback button to match design
- update job queue notification
- clickable queue spinner item to allow clear queue
- refactor mode out of store to specific workflow instance
- support different saved vs active mode
- other styling/layout tweaks
## Changes
- **What**: Changes the store to a composable and moves the mode state
to the workflow.
- This enables switching between tabs and maintaining the mode they were
in
## Screenshots (if applicable)
<img width="1866" height="1455" alt="image"
src="https://github.com/user-attachments/assets/f9a8cd36-181f-4948-b48c-dd27bd9127cf"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9137-App-mode-more-updates-fixes-3106d73d365081a18ccff6ffe24fdec7)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Remove Tailwind `@apply` from Vue styles across `src/` and
`apps/desktop-ui/src/` to align with Tailwind v4 guidance, replacing
usages with template utilities or native CSS while preserving behavior.
## Changes
- **What**:
- Batch 1: migrated low-risk template/style utility bundles out of
`@apply`.
- Batch 2: converted PrimeVue/`:deep()` override `@apply` blocks to
native CSS declarations.
- Batch 3: converted `src/components/node/NodeHelpContent.vue` markdown
styling from `@apply` to native CSS/token-based declarations.
- Batch 4: converted final desktop pseudo-element `@apply` styles and
removed stale `@reference` directives no longer required.
- Verified `rg -n "^\s*@apply\b" src apps -g "*.vue"` has no real CSS
`@apply` directives remaining (only known template false-positive event
binding in `NodeSearchContent.vue`).
## Review Focus
- Visual parity in components that previously depended on `@apply` in
`:deep()` selectors and markdown content:
- topbar tabs/popovers, dialogs, breadcrumb, terminal overrides
- desktop install/dialog/update/maintenance surfaces
- node help markdown rendering
- Confirm no regressions from removal of now-unneeded `@reference`
directives.
## Screenshots (if applicable)
- No new screenshots included in this PR.
- Screenshot Playwright suite was run with `--grep="@screenshot"` and
reports baseline diffs in this environment (164 passed, 39 failed, 3
skipped) plus a teardown `EPERM` restore error on local path
`C:\Users\DrJKL\ComfyUI\LTXV\user`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9146-fix-eradicate-tailwind-apply-usage-in-vue-styles-3116d73d3650813d8642e0bada13df32)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
Replace the Proxy-based proxy widget system with a store-driven
architecture where `promotionStore` and `widgetValueStore` are the
single sources of truth for subgraph widget promotion and widget values,
and `SubgraphNode.widgets` is a synthetic getter composing lightweight
`PromotedWidgetView` objects from store state.
## Motivation
The subgraph widget promotion system previously scattered state across
multiple unsynchronized layers:
- **Persistence**: `node.properties.proxyWidgets` (tuples on the
LiteGraph node)
- **Runtime**: Proxy-based `proxyWidget.ts` with `Overlay` objects,
`DisconnectedWidget` singleton, and `isProxyWidget` type guards
- **UI**: Each Vue component independently calling `parseProxyWidgets()`
via `customRef` hacks
- **Mutation flags**: Imperative `widget.promoted = true/false` set on
`subgraph-opened` events
This led to 4+ independent parsings of the same data, complex cache
invalidation, and no reactive contract between the promotion state and
the rendering layer. Widget values were similarly owned by LiteGraph
with no Vue-reactive backing.
The core principle driving these changes: **Vue owns truth**. Pinia
stores are the canonical source; LiteGraph objects delegate to stores
via getters/setters; Vue components react to store state directly.
## Changes
### New stores (single sources of truth)
- **`promotionStore`** — Reactive `Map<NodeId, PromotionEntry[]>`
tracking which interior widgets are promoted on which SubgraphNode
instances. Graph-scoped by root graph ID to prevent cross-workflow state
collision. Replaces `properties.proxyWidgets` parsing, `customRef`
hacks, `widget.promoted` mutation, and the `subgraph-opened` event
listener.
- **`widgetValueStore`** — Graph-scoped `Map<WidgetKey, WidgetState>`
that is the canonical owner of widget values. `BaseWidget.value`
delegates to this store via getter/setter when a node ID is assigned.
Eliminates the need for Proxy-based value forwarding.
### Synthetic widgets getter (SubgraphNode)
`SubgraphNode.widgets` is now a getter that reads
`promotionStore.getPromotions(rootGraphId, nodeId)` and returns cached
`PromotedWidgetView` objects. No stubs, no Proxies, no fake widgets
persisted in the array. The setter is a no-op — mutations go through
`promotionStore`.
### PromotedWidgetView
A class behind a `createPromotedWidgetView` factory, implementing the
`PromotedWidgetView` interface. Delegates value/type/options/drawing to
the resolved interior widget and stores. Owns positional state (`y`,
`computedHeight`) for canvas layout. Cached by
`PromotedWidgetViewManager` for object-identity stability across frames.
### DOM widget promotion
Promoted DOM widgets (textarea, image upload, etc.) render on the
SubgraphNode surface via `positionOverride` in `domWidgetStore`.
`DomWidgets.vue` checks for overrides and uses the SubgraphNode's
coordinates instead of the interior node's.
### Promoted previews
New `usePromotedPreviews` composable resolves image/audio/video preview
widgets from promoted entries, enabling SubgraphNodes to display
previews of interior preview nodes.
### Deleted
- `proxyWidget.ts` (257 lines) — Proxy handler, `Overlay`,
`newProxyWidget`, `isProxyWidget`
- `DisconnectedWidget.ts` (39 lines) — Singleton Proxy target
- `useValueTransform.ts` (32 lines) — Replaced by store delegation
### Key architectural changes
- `BaseWidget.value` getter/setter delegates to `widgetValueStore` when
node ID is set
- `LGraph.add()` reordered: `node.graph` assigned before widget
`setNodeId` (enables store registration)
- `LGraph.clear()` cleans up graph-scoped stores to prevent stale
entries across workflow switches
- `promotionStore` and `widgetValueStore` state nested under root graph
UUID for multi-workflow isolation
- `SubgraphNode.serialize()` writes promotions back to
`properties.proxyWidgets` for persistence compatibility
- Legacy `-1` promotion entries resolved and migrated on first load with
dev warning
## Test coverage
- **3,700+ lines of new/updated tests** across 36 test files
- **Unit**: `promotionStore.test.ts`, `widgetValueStore.test.ts`,
`promotedWidgetView.test.ts` (921 lines),
`subgraphNodePromotion.test.ts`, `proxyWidgetUtils.test.ts`,
`DomWidgets.test.ts`, `PromotedWidgetViewManager.test.ts`,
`usePromotedPreviews.test.ts`, `resolvePromotedWidget.test.ts`,
`subgraphPseudoWidgetCache.test.ts`
- **E2E**: `subgraphPromotion.spec.ts` (622 lines) — promote/demote,
manual/auto promotion, paste preservation, seed control augmentation,
image preview promotion; `imagePreview.spec.ts` extended with
multi-promoted-preview coverage
- **Fixtures**: 2 new subgraph workflow fixtures for preview promotion
scenarios
## Review focus
- Graph-scoped store keying (`rootGraphId`) — verify isolation across
workflows/tabs and cleanup on `LGraph.clear()`
- `PromotedWidgetView` positional stability — `_arrangeWidgets` writes
to `y`/`computedHeight` on cached objects; getter returns fresh array
but stable object references
- DOM widget position override lifecycle — overrides set on promote,
cleared on demote/removal/subgraph navigation
- Legacy `-1` entry migration — resolved and written back on first load;
unresolvable entries dropped with dev warning
- Serialization round-trip — `promotionStore` state →
`properties.proxyWidgets` on serialize, hydrated back on configure
## Diff breakdown (excluding lockfile)
- 153 files changed, ~7,500 insertions, ~1,900 deletions (excluding
pnpm-lock.yaml churn)
- ~3,700 lines are tests
- ~300 lines deleted (proxyWidget.ts, DisconnectedWidget.ts,
useValueTransform.ts)
<!-- Fixes #ISSUE_NUMBER -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8856-feat-synthetic-widgets-getter-for-SubgraphNode-proxy-widget-v2-3076d73d365081c7b517f5ec7cb514f3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Extracts error-related state and logic from `executionStore` into a
dedicated `executionErrorStore` for better separation of concerns.
## Changes
- **New store**: `executionErrorStore` with all error state
(`lastNodeErrors`, `lastExecutionError`, `lastPromptError`), computed
properties (`hasAnyError`, `totalErrorCount`,
`activeGraphErrorNodeIds`), and UI state (`isErrorOverlayOpen`,
`showErrorOverlay`, `dismissErrorOverlay`)
- **Moved util**: `executionIdToNodeLocatorId` extracted to
`graphTraversalUtil`, reusing `traverseSubgraphPath` and accepting
`rootGraph` as parameter
- **Updated consumers**: 12 files updated to import from
`executionErrorStore`
- **Backward compat**: Deprecated getters retained in `ComfyApp` for
extension compatibility
## Review Focus
- Deprecated getters in `app.ts` — can be removed in a future
breaking-change PR once extension authors migrate
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9060-refactor-Extract-executionErrorStore-from-executionStore-30e6d73d36508101973de835ab6b199f)
by [Unito](https://www.unito.io)
## Summary
Completes the workflow persistence overhaul by integrating the new draft
system into the app and migrating existing data. Fixes two critical
bugs:
1. **QuotaExceededError** - localStorage fills up with workflow drafts,
breaking auto-save
2. **Cross-workspace data leakage** - Drafts from one ComfyUI instance
appear in another
## Changes
- **What**:
- `useWorkflowPersistenceV2.ts` - Main composable that hooks into graph
changes with 512ms debounce
- `migrateV1toV2.ts` - One-time migration of existing drafts to the new
scoped format
- Updated E2E tests for new storage key patterns
- **Why**: Users lose work when storage quota is exceeded, and see
confusing workflows from other instances
## How It Works
- **Workspace scoping**: Each ComfyUI instance (identified by server
URL) has isolated draft storage
- **LRU eviction**: When storage is full, oldest drafts are
automatically removed (keeps 32 most recent)
- **Tab isolation**: Each browser tab tracks its own active/open
workflows via sessionStorage
- **Debounced saves**: Graph changes are batched with 512ms delay to
reduce storage writes
## Migration
Existing V1 drafts are automatically migrated on first load. The
migration:
1. Reads drafts from old `Comfy.Workflow.*` keys
2. Converts to new workspace-scoped format
3. Cleans up old keys after successful migration
---
*Part 4 of 4 in the workflow persistence improvements stack*
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
Sometimes, middle mouse clicks would fail to initiate a canvas pan,
depending on the target of the initial pan. This PR adds a capturing
event handler to the transform pane that forwards the pointer event to
canvas if
- It is a middle mouse click
- The target element is not a focused text element
Resolves#6911
While testing this, I encountered infrequent cases of "some nodes
unintentionally translating continually to the left". Reproduction was
too unreliable to properly track down, but did appear unrelated to this
PR.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8865-A-heavy-handed-fix-for-middlemouse-pan-3076d73d365081ea9a4ddd5786fc647a)
by [Unito](https://www.unito.io)
## Summary
- Move `NodeContextMenu` from `SelectionToolbox.vue` to
`GraphCanvas.vue` so the right-click context menu renders independently
of the `Comfy.Canvas.SelectionToolbox` setting
- Fixes#8417
## Test plan
- [x] Disable selection toolbox in settings, right-click a node —
context menu appears
- [x] Enable selection toolbox, right-click a node — context menu still
appears
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8781-fix-right-click-context-menu-disabled-when-selection-toolbox-is-off-3036d73d36508168a9add58e060b7e93)
by [Unito](https://www.unito.io)
## Summary
Fix all i18n `no-restricted-imports` lint warnings and upgrade rules
from `warn` to `error`.
## Changes
- **What**: Migrate Vue components from `import { t/d } from '@/i18n'`
to `const { t } = useI18n()`. Migrate non-component `.ts` files from
`useI18n()` to `import { t/d } from '@/i18n'`. Allow `st` import from
`@/i18n` in Vue components (it wraps `te`/`t` for safe fallback
translation). Remove `@deprecated` tag from `i18n.ts` global exports
(still used by `st` and non-component code). Upgrade both lint rules
from `warn` to `error`.
## Review Focus
- The `st` helper is intentionally excluded from the Vue component
restriction since it provides safe fallback translation needed for
custom node definitions.
Fixes#8701
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8704-fix-resolve-i18n-no-restricted-imports-lint-warnings-2ff6d73d365081ae84d8eb0dfef24323)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>