## Summary
- Use the last previewable output as the batch cover/thumbnail instead
of the first, so the most recently generated image (e.g., `_00010`) is
shown as the representative
- Reverse output order in batch folder view so newest images appear at
the top
- Gitignore `.claude/worktrees` to fix knip scanning untracked worktree
copies
## Linked Issues
- Fixes#9354
- Related to #9080
## Test plan
- [ ] Generate a batch of images (e.g., 10 images) and verify the
sidebar shows the last generated image as the cover
- [ ] Expand the batch folder view and verify images are in
reverse-chronological order (newest first)
- [ ] Verify existing unit tests pass (`pnpm test:unit`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9467-fix-show-most-recent-image-first-in-asset-sidebar-batch-view-31b6d73d365081cbaf30f81009c7fcfa)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Fixes#9319
- Add [fast-check](https://github.com/dubzzz/fast-check) property-based
testing with FSM (Finite State Machine) traversal to automatically
explore state combinations in the workflow persistence system
- Fix a real bug in `saveDraft()` discovered by the FSM test: orphan
cleanup in `loadIndex()` could delete a just-written payload when the
in-memory cache was empty
## Why this is needed
#9317 exposed a class of bug where two independently correct changes
interact to cause workflow loss. Conventional unit tests verify
specific, hand-picked scenarios and cannot catch these cross-PR
interaction bugs.
### AS IS (before)
| Aspect | Status |
|---|---|
| Testing approach | Example-based: developer picks specific inputs and
expected outputs |
| State coverage | Only explicitly written scenarios are tested |
| Cross-interaction bugs | Not detectable — each test runs one isolated
path |
| Bug in `saveDraft` | Undetected — `loadIndex()` orphan cleanup could
delete a just-written payload after `reset()` |
### TO BE (after)
| Aspect | Status |
|---|---|
| Testing approach | Property-based: fast-check generates **200 random
command sequences** per run |
| State coverage | Random exploration of `SaveDraft → GetDraft →
RemoveDraft → MoveDraft → GetMostRecentPath → Reset` combinations |
| Cross-interaction bugs | Detected automatically — fast-check shrinks
failing sequences to minimal reproductions |
| Bug in `saveDraft` | Found and fixed — `loadIndex()` now runs
**before** `writePayload()` to prevent orphan cleanup race |
## What fast-check does
fast-check is a property-based testing library. Instead of testing "does
this specific input produce this specific output?", it tests "does this
**property** hold for **all possible inputs**?"
For FSM testing specifically, fast-check:
1. Takes a set of **commands** (SaveDraft, GetDraft, RemoveDraft,
MoveDraft, GetMostRecentPath, Reset)
2. Generates **random sequences** of these commands
3. Runs each sequence against both a **model** (simplified oracle) and
the **real system** (store + localStorage)
4. Verifies **invariants** after every mutating command (index/payload
consistency, no orphans, LRU correctness, model agreement)
5. When a failure is found, **shrinks** the sequence to the minimal
reproduction
Example: the bug this PR fixes was shrunk to just 4 commands:
```
SaveDraft(d.json) → RemoveDraft(d.json) → Reset() → SaveDraft(a.json) ✗
```
## Changes
| File | Change |
|---|---|
| `package.json` / `pnpm-workspace.yaml` | Add `fast-check`
devDependency |
| `draftCacheV2.property.test.ts` | 7 property tests for pure index
functions |
| `workflowDraftStoreV2.fsm.test.ts` | FSM test: 6 commands, invariant
checking, 200 runs |
| `workflowDraftStoreV2.ts` | Fix: move `loadIndex()` before
`writePayload()` in `saveDraft()` |
## Test plan
- [x] `pnpm test:unit` — all 117 persistence tests pass (including 7
property + 1 FSM)
- [x] `pnpm typecheck` — clean
- [x] `pnpm lint` — clean
- [x] Pre-commit hooks pass (format, lint, typecheck)
- [x] Pre-push hook passes (knip)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9370-test-add-property-based-FSM-tests-for-workflow-persistence-3196d73d3650813daa98cdd8bef7e975)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Right-clicking a textarea widget (e.g. text node) shows the browser's
native context menu instead of ComfyUI's context menu, preventing access
to promote/un-promote options in subgraphs.
## Changes
- **What**: Replace `@contextmenu.capture.stop` on
`WidgetTextarea.vue`'s `<Textarea>` with a handler implementing
double-right-click toggling: first right-click shows ComfyUI's context
menu, second right-click (while menu is open) allows browser native
menu. Exposes `isNodeOptionsOpen()` from `useMoreOptionsMenu.ts` to
check menu state.
## Review Focus
The capture-phase handler in `WidgetTextarea.vue` only changes
`contextmenu` handling — pointer event modifiers
(`pointerdown/move/up.capture.stop`) that prevent canvas panning are
untouched. The double-right-click pattern matches Notion/YouTube
behavior for editable text fields.
<!-- Pipeline-Ticket: d7a53160-e1e1-42bb-a5ac-c0c2702c629c -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9840-fix-show-ComfyUI-context-menu-on-textarea-widget-right-click-3216d73d36508102b4c9c13a5915bc48)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Adds VS Code-style multi-keybinding support to the Keybinding settings
panel. Commands can now have multiple keybindings displayed, expanded,
and individually managed.
- Fixes#1088
## Changes
### Store (`keybindingStore.ts`)
- `removeAllKeybindingsForCommand(commandId)` — unsets all bindings for
a command
- `updateSpecificKeybinding(old, new)` — replaces a single binding
without affecting others
- `resetKeybindingForCommand` — updated to restore **all** default
bindings, not just the first
- `isCommandKeybindingModified` — updated to compare full sorted sets of
bindings
### UI (`KeybindingPanel.vue`)
- **Data model**: `keybinding: KeybindingImpl | null` → `keybindings:
KeybindingImpl[]`
- **Multi-binding display**: shows up to 2 combos inline with `, `
separator, then `+ N more` badge
- **Expand/collapse**: click any row with 2+ bindings to expand
individual binding rows; chevron-right icon rotates on expand
- **Per-binding actions**: edit (pencil), reset, trash on each expanded
sub-row
- **Parent row actions**: `+`/trash for 2+ bindings, pencil/reset/trash
for 1, `+`/disabled for 0
- **Edit modes**: `edit` (replace specific binding via
`updateSpecificKeybinding`) and `add` (append via `addUserKeybinding`)
- **Right-click context menu**: Change keybinding, Add new, Reset to
default, Remove keybinding — with proper disabled states and lucide
icons
- **Remove all dialog**: confirmation via `showSmallLayoutDialog` with
`RemoveAllKeybindingsHeader`/`Content` components
- **Reset all dialog**: confirmation via `showConfirmDialog` before
resetting all keybindings to defaults
- **Double-click**: 0 bindings → add, 1 → edit, 2+ → no-op (single click
toggles expand)
- **Consistent alignment**: commands without chevron get `pl-5` padding
to align with those that have it
### Tests (`keybindingStore.test.ts`)
- 7 new tests covering `removeAllKeybindingsForCommand`,
`updateSpecificKeybinding`, multi-binding `isCommandKeybindingModified`,
and multi-binding `resetKeybindingForCommand`
### i18n (`main.json`)
- 11 new keys: removeAllKeybindingsTitle/Message, removeAll,
changeKeybinding, addNewKeybinding, resetToDefault, removeKeybinding,
nMoreKeybindings, resetAllKeybindingsTitle/Message, allKeybindingsReset
### New components
- `RemoveAllKeybindingsHeader.vue` — dialog header
- `RemoveAllKeybindingsContent.vue` — dialog body with Close/Remove all
buttons
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes (no new errors)
- [x] `pnpm vitest run src/platform/keybindings/` — 45 tests pass
- [x] CodeRabbit review — 0 findings
- [ ] Manual: open Settings → Keybindings, verify multi-binding commands
(e.g. Delete Selected Items, Zoom In) show multiple combos
- [ ] Manual: click row to expand, verify per-binding actions work
- [ ] Manual: right-click row, verify context menu actions
- [ ] Manual: click trash on 2+ binding command, verify "Remove all"
confirmation dialog
- [ ] Manual: click "Reset All" button, verify confirmation dialog
appears
- [ ] Manual: add/edit/remove individual bindings, verify persistence
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9738-feat-multi-keybinding-support-in-settings-panel-3206d73d365081e9b08bd3cfe21495f1)
by [Unito](https://www.unito.io)
## Summary
Unify the search bar + action buttons layout across all left sidebar
panels (Node Library, Workflows, Model Library, Media Assets) using a
shared `SidebarTopArea` presentation component.
## Changes
- **What**:
- Add `SidebarTopArea.vue` — layout component with `flex-1` default slot
(search) and `#actions` slot (buttons), plus optional `bottomDivider`
prop
- Replace raw `<button>` elements in Node Library with `<Button
variant="secondary" size="icon">`
- Replace reka-ui `TabsTrigger` with shared `Tab/TabList` component in
Node Library
- Move Media Assets tab list from hover-only `#tool-buttons` to
always-visible header below search area
- Unify spacing (`gap-2`, `p-2 2xl:px-4`) and divider styles across all
sidebar panels
- Remove unused `assetType` prop and header from
`AssetsSidebarGridView`/`AssetsSidebarListView`
## Review Focus
- `SidebarTopArea` API simplicity — just slots + one optional prop
- Node Library still requires `TabsRoot` in the body for reka-ui
`TabsContent` in child panels
- Media Assets tabs are now always visible instead of hover-only
[screen-capture
(1).webm](https://github.com/user-attachments/assets/fe1d8f7b-5674-4bb3-9842-569e4c3af6c9)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9740-feat-unify-sidebar-panel-header-layout-with-SidebarTopArea-component-3206d73d365081ea8ba7fd6ac54e0169)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Fixes a bug where widgets marked as `advanced` were always visible,
ignoring the "Always show advanced widgets on all nodes" setting
- Root cause: `extractWidgetDisplayOptions` in `useGraphNodeManager.ts`
read `widget.advanced` (always `undefined` on BaseWidget) instead of
`widget.options?.advanced` (where `litegraphService` actually sets the
flag)
- Consistent with how `hidden` is already read from
`widget.options.hidden` on the adjacent line
## Test plan
- [ ] Load a node with advanced inputs (e.g. `LTXVScheduler`)
- [ ] Verify `max_shift`, `base_shift`, `stretch`, `terminal` are hidden
when "Always show advanced widgets on all nodes" is disabled
- [ ] Verify they become visible when the setting is enabled or the
per-node toggle is clicked
- [ ] Verify the advanced toggle button appears on nodes with advanced
widgets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9857-fix-advanced-widgets-always-visible-regardless-of-setting-3226d73d36508132b338d098fbf19433)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Add a setting to select all children (nodes, reroutes, nested groups)
when clicking a group on the canvas.
## Changes
- **What**: New `LiteGraph.Group.SelectChildrenOnClick` boolean setting
(default: `false`). When enabled, selecting a group cascades `select()`
to all its `_children`, and deselecting cascades `deselect()`. Recursion
handles nested groups naturally. No double-move risk — the drag handler
already uses `skipChildren=true`. The setting is wired via `onChange` to
`canvas.groupSelectChildren`, keeping litegraph free of platform
imports.
## Review Focus
- The select/deselect cascading in `LGraphCanvas.select()` /
`deselect()` — verify no infinite recursion risk with deeply nested
groups.
- The `groupSelectChildren` property is set via the setting's `onChange`
callback on `LGraphCanvas.active_canvas` — confirm this covers canvas
re-creation scenarios.
## Screenshots (if applicable)
N/A — behavioral change behind a setting toggle.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9149-feat-select-group-children-on-click-3116d73d365081a1a7b8c82dea95b242)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.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
- Replace PrimeVue `ColorPicker` with a custom component built on Reka
UI Popover
- New `ColorPicker` supports HSV saturation-value picking, hue/alpha
sliders, hex/rgba display toggle
- Simplify `WidgetColorPicker` by removing PrimeVue-specific
normalization logic
- Add Storybook stories for both `ColorPicker` and `WidgetColorPicker`
## Test plan
- [x] Unit tests pass (9 widget tests, 47 colorUtil tests)
- [x] Typecheck passes
- [x] Lint passes
- [ ] Verify color picker visually in Storybook
- [ ] Test color picking in node widgets with hex/rgb/hsb formats
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9647-feat-replace-PrimeVue-ColorPicker-with-custom-component-31e6d73d36508114bc54d958ff8d0448)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Inline splash screen CSS into `index.html` to fix broken loading
animation on cloud/ephemeral environments.
## Changes
- **What**: On cloud/ephemeral environments (e.g.
`fe-pr-*.testenvs.comfy.org`), SPA fallback serves `index.html` for
unknown paths. The `<link href="splash.css">` request resolves to
`/cloud/splash.css`, which the server does not find as a static file —
so it returns `index.html` with `200 OK`. The browser receives HTML
instead of CSS, the CSS parser silently ignores it, and the splash
screen renders without any styles or animations.
- Inlined `splash.css` directly into `index.html` `<style>` block —
eliminates the external request entirely
- Moved `splash.css` to `src/assets/` for content-hashed Vite processing
as source of truth
- Removed `public/splash.css`
## Review Focus
- The inline CSS is byte-for-byte identical to the original
`public/splash.css`
- `src/assets/splash.css` preserved as canonical source for future
changes
[screen-capture
(1).webm](https://github.com/user-attachments/assets/06729641-d1fd-47aa-9dd4-4acd28c2cfcf)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9849-fix-inline-splash-CSS-to-prevent-SPA-fallback-breakage-on-cloud-environments-3226d73d365081418741eb0944a74977)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fix download icon not appearing when file size is successfully fetched
in the missing models dialog.
## Changes
- **What**: Restructured the `v-if/v-else-if` chain in
`MissingModelsContent.vue` so that file size and download icon render
together instead of being mutually exclusive. Previously, a successful
file size fetch would prevent the download button from rendering.
## Review Focus
The file size span and download/gated-link are now inside a shared
`<template v-else-if="model.isDownloadable">` block. File size uses
`v-if` (independent), while gated link and download button remain
`v-if/v-else` (mutually exclusive with each other).
[screen-capture.webm](https://github.com/user-attachments/assets/f2f04d52-265b-4d05-992e-0ffe9bf64026)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9850-fix-show-download-icon-alongside-file-size-in-missing-models-dialog-3226d73d365081fd943bcfdedda87c73)
by [Unito](https://www.unito.io)
## Summary
Bake the frontend git commit hash into the build so it no longer needs
to be fetched from the server via `/api/system_stats`.
## Changes
- **What**: Add `__COMFYUI_FRONTEND_COMMIT__` build-time constant (via
Vite `define`) sourced from `git rev-parse HEAD` at build time. Falls
back to `"unknown"` if git is unavailable. `SystemStatsPanel` uses this
baked-in value for the "Frontend Version" row in cloud mode instead of
the server-provided `comfyui_frontend_version` field.
## Testing
Confirmed to display the actual commit.
<img width="1448" height="908" alt="Screenshot 2026-03-12 at 7 09 52 PM"
src="https://github.com/user-attachments/assets/2b42348a-5c3e-4509-aa84-1a259bba5f3f"
/>
## Review Focus
- The `getDisplayValue` override for `comfyui_frontend_version` —
cleanest way to swap the data source without restructuring the column
system.
- No cloud-side changes needed: the `sync-frontend-build` workflow
already checks out the frontend repo at the exact commit ref, so `git
rev-parse HEAD` returns the correct hash.
## Summary
Fix asset browser sidebar missing categories because `typeCategories`
assumed a fixed tag order from the API.
## Changes
- **What**: Replace `tags[0] === 'models'` / `tags[1]` with
`tags.includes(MODELS_TAG)` and `flatMap`+`filter`, matching the pattern
used by `getAssetModelFolders` and `filterByCategory`.
## Review Focus
The API returns tags in arbitrary order (e.g. `['checkpoints',
'models']` instead of `['models', 'checkpoints']`). The old code
filtered out most assets, resulting in an empty sidebar. New test
validates arbitrary tag ordering.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9843-fix-use-order-independent-tag-matching-in-asset-browser-categories-3216d73d365081b886f3d5ab1790e19d)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
This has semi-significant performance impact if you use the same
workflow for more than a day. I left Comfy running with a mouse jiggler
and this reduced my instance to a crawl after about an hour.
`nodeProgressStatesByJob` accumulated an entry for every job that ever
executed during a session. In long-running sessions this grew without
bound, since entries were only removed for the active job on
`resetExecutionState`.
Add `MAX_PROGRESS_JOBS` (1000) and `evictOldProgressJobs()`, called
after each `handleProgressState` update. When the map exceeds the limit,
the oldest entries (by ES2015+ insertion order) are pruned — keeping
only the most recent 1000. This mirrors the pattern used by
assetsStore's `MAX_HISTORY_ITEMS`.
Also adds tests for:
- nodeLocationProgressStates computed reactivity (recomputes on
wholesale replacement, produces new references)
- Eviction behavior (retains below limit, evicts oldest above limit,
preserves most recent, no-op when updating existing job)
- API event handler wiring via captured apiEventHandlers map
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9249-fix-cap-nodeProgressStatesByJob-to-prevent-unbounded-memory-growth-3136d73d365081e49b36d8ade0d4dd6e)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Muted (NEVER mode) subgraph nodes throw "No inner node DTO found" during
prompt serialization because `resolveOutput()` falls through to subgraph
resolution for nodes whose inner DTOs were never registered.
## Changes
- **What**: Add early return in `ExecutableNodeDTO.resolveOutput()` for
`NEVER` mode nodes, matching the existing `BYPASS` mode guard. Add 5
tests covering muted, bypassed, and normal mode resolution.
## Review Focus
The fix is a single-line early return. The key insight is that
`graphToPrompt` in `executionUtil.ts` correctly skips `getInnerNodes()`
for muted/bypassed nodes, so their inner DTOs are never in the map — but
`resolveOutput()` was missing the corresponding guard for `NEVER` mode.
Fixes#8986
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9302-fix-return-undefined-for-muted-node-output-resolution-3156d73d3650811e9697c7281f11cf96)
by [Unito](https://www.unito.io)
## Summary
Adds `auxclick` event listener to prevent the browser's default
middle-click paste behavior on Linux systems.
**Problem:** On Linux, middle-clicking anywhere triggers a paste from
the PRIMARY clipboard. When middle-dragging to pan the canvas, this
causes the entire workflow to be duplicated as new nodes on mouse
release.
**Solution:** Add `auxclick` event listener with `preventDefault()` to
the graph canvas, blocking the paste while preserving pan functionality.
## Changes
- Add `auxclick` event listener in `bindEvents()`
- Add corresponding `removeEventListener` in `unbindEvents()`
## Test Plan
- [ ] On Linux: Middle-drag to pan canvas - should pan without
duplicating nodes
- [ ] On Linux: Verify left/right click behaviors unchanged
- [ ] On Windows/macOS: Verify no regression (auxclick should have no
effect)
Fixes#4464
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8259-fix-prevent-middle-click-paste-duplicating-workflow-on-Linux-2f16d73d3650812b98f9cada699f5508)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Update workspace creation modal copy to clarify that creating a
workspace establishes a **new** credit pool, rather than sharing from
the owner's existing credits.
## Changes
- **What**: Changed i18n message from "Workspaces let members share a
single credits pool" to "Workspaces create a new credit pool that can be
shared among members"
## Review Focus
Copy change only — single i18n string update in
`src/locales/en/main.json`.
Fixes COM-16521
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9811-fix-update-workspace-creation-modal-phrasing-for-credit-pool-clarity-3216d73d36508186850ac5a8ad97461d)
by [Unito](https://www.unito.io)
## Summary
- When bulk exporting, `job_asset_name_filters` was always sent for
every job, restricting each job to only the assets the user clicked on.
For multi-output jobs, this meant the ZIP only contained 1 asset per job
instead of all outputs.
- Now compares selected asset count per job against `outputCount`
metadata and omits the filter for fully-selected jobs, so the backend
returns all assets.
## Test plan
- [x] Unit tests: all outputs selected → no filter sent
- [x] Unit tests: subset selected → filter sent
- [x] Unit tests: mixed selection → filter only for partial jobs
- [x] Unit tests: multiple fully-selected jobs → no filters
- [x] Typecheck, lint pass
- [ ] Manual: bulk export multi-output jobs in cloud env, verify ZIP
contains all outputs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9684-fix-omit-job_asset_name_filters-when-all-job-outputs-selected-31f6d73d3650814482f8c59d05027d79)
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
nodeLocationProgressStates runs executionIdToNodeLocatorId for every
ancestor prefix of every node's display_node_id on every WebSocket
progress update. Each call traverses the subgraph hierarchy via
getNodeById. For nested subgraphs with many executing nodes, this
results in O(N × D²) graph lookups per progress tick, where N is
the number of executing nodes and D is the nesting depth.
Add a plain Map cache inside the execution store that memoizes
executionIdToNodeLocatorId results by execution ID string. The
cache persists across computed re-evaluations within a single
execution run, reducing subsequent progress updates to O(1)
lookups per execution ID.
Cache invalidation:
- Cleared at execution start (handleExecutionStart) to ensure
fresh graph state for each new run
- Cleared at execution end (resetExecutionState) to prevent
stale entries leaking across runs
The cache stores strings only (no graph node references), with
typical size of ~50-200 entries per run, so memory impact is
negligible.
- **What**: Caches a mapping of ids to ids, preventing an exponential
re-scan of subgraphs
- **Breaking**: Nothing
- **Dependencies**: None
You will need to set the feature flag
`ff:expose_executionId_to_node_locator_id_cache_counters` from the
underlying counter PR if you want to measure the impact.
```js
localStorage.setItem(
'ff:expose_executionId_to_node_locator_id_cache_counters',
'true'
)
```
## Review Focus
Before pulling this PR, pull
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9243 , set the
feature flag, and run a workflow with nested subgraphs, then look in
console to see a cache measurement.
Next, pull this PR and load the same workflow. Note the massive
reduction in visits.
## Screenshots
Login problems due to cross-opener policy are preventing me from taking
screenshots from a local dev build at this time
## Thread
There isn't one. I don't have access to AmpCode or Unito.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9244-Cache-execution-id-to-node-locator-id-mappings-3136d73d365081e680eae3c891480ee7)
by [Unito](https://www.unito.io)
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Add Sentry breadcrumbs to subgraph proxy widget operations for better
observability of widget state changes.
## Changes
- **What**: Add `Sentry.addBreadcrumb()` calls with category
`'subgraph'` to `promoteWidget`, `demoteWidget`, and `pruneDisconnected`
in `proxyWidgetUtils.ts`
## Review Focus
Breadcrumbs are info-level and don't affect control flow. They log
widget name/node ID for promote/demote and removed count for prune.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8996-chore-add-Sentry-breadcrumbs-to-subgraph-proxy-widget-operations-30d6d73d365081a5abbccabd39fc7129)
by [Unito](https://www.unito.io)
## 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>
Previously, MatchType and Autogrow inputs would not be considered would
filtering searchbox entires. For example, "Batch Images" would not show
as a suggestion would dragging a noodle from a "Load Image" node.
This is resolved by adding a step during nodeDef registration to
precalculate a list of all input types. This may have performance
implications.
- Search filtering should be more performant
- Initial node registration will be slower
- There's additional memory cost to store this information on every
node.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9388-Support-search-filtering-to-dynamic-input-types-3196d73d365081d9939eff5e167a7e83)
by [Unito](https://www.unito.io)
## Summary
Centralize the inline `display_name || name` pattern into
`getAssetDisplayName`, adding `display_name` to the existing metadata
fallback chain.
## Changes
- **What**: Update `getAssetDisplayName` fallback chain to
`user_metadata.name → metadata.name → display_name → name`. Replace all
6 inline `asset.display_name || asset.name` call sites with the shared
utility. Remove duplicate local function in `AssetsSidebarListView.vue`.
## Review Focus
The fallback order preserves user_metadata overrides while incorporating
the `display_name` field added in #9626. All callers now go through a
single code path.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9641-refactor-centralize-display_name-name-into-getAssetDisplayName-31e6d73d365081e09e5de85486583443)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Wires the nightly survey system into the app by adding a controller
component and a convenience composable for feature-site usage tracking.
## Changes
- **What**: NightlySurveyController iterates enabled surveys from the
registry and renders a NightlySurveyPopover for each.
useSurveyFeatureTracking wraps useFeatureUsageTracker with a
config-enabled guard for use at feature call sites.
- **Tree-shaking**: Controller is loaded via defineAsyncComponent behind
a compile-time isNightly/isCloud/isDesktop guard in SideToolbar.vue, so
the entire survey module subtree is eliminated from cloud/desktop/stable
builds.
## Review Focus
- DCE pattern: controller imported conditionally via
defineAsyncComponent + distribution guard (same pattern as
ComfyRunButton/index.ts)
- useSurveyFeatureTracking short-circuits early when config is
absent/disabled (avoids initializing tracker storage)
- No user-facing behavior change: FEATURE_SURVEYS registry is still
empty
## Part of Nightly Survey System
This is part 5 of a stacked PR chain:
1. feat/feature-usage-tracker - useFeatureUsageTracker (merged in #8189)
2. feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3. feat/survey-config - surveyRegistry.ts (#8355, merged)
4. feat/survey-popover - NightlySurveyPopover.vue (#9083, merged)
5. **feat/survey-integration** - NightlySurveyController.vue (this PR)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Fix input asset previews (images/videos) disappearing from
LoadImage/LoadVideo nodes after execution and a browser tab switch.
## Changes
- **What**: Guard `setOutputsByLocatorId` in `imagePreviewStore` to
preserve existing input-type preview images (`type: 'input'`) when the
incoming execution output has no images. Execution outputs with actual
images still overwrite as expected.
## Review Focus
- The guard only applies when existing output is an input preview (`type
=== 'input'` for all images) AND incoming output has no images — this is
the exact scenario where execution clobbers upload widget previews.
- Root cause: execution results from the backend overwrite the upload
widget's synthetic preview for LoadImage/LoadVideo nodes (which produce
no output images). Combined with the deferred resize-observer
re-observation from PR #8805, returning to a hidden tab reads the
now-empty store entry.
## Summary
- When a media node (LoadImage/LoadAudio/LoadVideo) is selected and the
clipboard contains stale node metadata from a prior Ctrl+C, pasting
skips the node-metadata deserialization so that the paste falls through
to litegraph's default handler instead of incorrectly pasting the old
copied node.
- FixesComfy-Org/ComfyUI#12896
## Root Cause
The paste handler in `usePaste.ts` checks clipboard `text/html` for
`data-metadata` (serialized node data) **before** falling through to
litegraph's default paste. When a user copies a node, then copies a web
image, the browser clipboard may retain the old `data-metadata` in
`text/html` while the image data is not available as a
`DataTransferItem` file. This causes the stale node to be pasted instead
of the image.
## Fix
Skip `pasteClipboardItems()` when a media node is selected, allowing the
paste to fall through to litegraph's default handler which can handle
the clipboard content appropriately.
## Test plan
- [x] Added unit test verifying node metadata paste is skipped when
media node is selected
- [x] Manual: Copy a node → copy a web image → select LoadImage node →
Ctrl+V → verify image is pasted, not the node
## AS IS
https://github.com/user-attachments/assets/210d77d3-5c49-4e38-91b7-b9d9ea0e7ca0
## TO BE
https://github.com/user-attachments/assets/b68e4582-0c57-48b8-9ed9-0b3243bb1554🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9773-fix-skip-node-metadata-paste-when-media-node-is-selected-3216d73d3650814d92dadcd0c0ec79c7)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Asset widget dropdown search only matched against `item.name`
(filename), but users see `item.label` (display name). Now searches both
fields so filtering matches what is visually displayed.
## Changes
- **What**: `defaultSearcher` in `FormDropdown` now matches against both
`name` and `label` fields
- Added 3 unit tests covering label-based search scenarios
## Review Focus
- The change only affects cloud asset mode where `name` (filename) and
`label` (display name) differ. In local mode, `label` is either
`undefined` or identical to `name`, so behavior is unchanged.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9774-bugfix-Asset-widget-search-matches-display-label-3216d73d365081ca8befdf7260c66a26)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@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
When a workflow is loaded with missing models, users currently have no
way to identify or resolve them from within the UI. This PR adds a full
missing-model detection and resolution pipeline that surfaces missing
models in the Errors tab, allowing users to install or import them
without leaving the editor.
## Changes
### Missing Model Detection
- Scan all COMBO widgets across root graph and subgraphs for model-like
filenames during workflow load
- Enrich candidates with embedded workflow metadata (url, hash,
directory) when available
- Verify asset-supported candidates against the asset store
asynchronously to confirm installation status
- Propagate missing model state to `executionErrorStore` alongside
existing node/prompt errors
### Errors Tab UI — Model Resolution
- Group missing models by directory (e.g. `checkpoints`, `loras`, `vae`)
with collapsible category cards
- Each model row displays:
- Model name with copy-to-clipboard button
- Expandable list of referencing nodes with locate-on-canvas button
- **Library selector**: Pick an alternative from the user's existing
models to substitute the missing model with one click
- **URL import**: Paste a Civitai or HuggingFace URL to import a model
directly; debounced metadata fetch shows filename and file size before
confirming; type-mismatch warnings (e.g. importing a LoRA into
checkpoints directory) are surfaced with an "Import Anyway" option
- **Upgrade prompt**: In cloud environment, free-tier subscribers are
shown an upgrade modal when attempting URL import
- Separate "Import Not Supported" section for custom-node models that
cannot be auto-resolved
- Status card with live download progress, completion, failure, and
category-mismatch states
### Canvas Integration
- Highlight nodes and widgets that reference missing models with error
indicators
- Propagate missing-model badges through subgraph containers so issues
are visible at every graph level
### Code Cleanup
- Simplify `surfacePendingWarnings` in workflowService, remove stale
widget-detected model merging logic
- Add `flattenWorkflowNodes` utility to workflowSchema for traversing
nested subgraph structures
- Extract `MissingModelUrlInput`, `MissingModelLibrarySelect`,
`MissingModelStatusCard` as focused single-responsibility components
## Testing
- Unit tests for scan pipeline (`missingModelScan.test.ts`): enrichment,
skip-installed, subgraph flattening
- Unit tests for store (`missingModelStore.test.ts`): state management,
removal helpers
- Unit tests for interactions (`useMissingModelInteractions.test.ts`):
combo select, URL input, import flow, library confirm
- Component tests for `MissingModelCard` and error grouping
(`useErrorGroups.test.ts`)
- Updated `workflowService.test.ts` and `workflowSchema.test.ts` for new
logic
## Review Focus
- Missing model scan + enrichment pipeline in `missingModelScan.ts`
- Interaction composable `useMissingModelInteractions.ts` — URL metadata
fetch, library install, upload fallback
- Store integration and canvas-level error propagation
## Screenshots
https://github.com/user-attachments/assets/339a6d5b-93a3-43cd-98dd-0fb00681b66f
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9743-feat-Surface-missing-models-in-Errors-tab-Cloud-3206d73d365081678326d3a16c2165d8)
by [Unito](https://www.unito.io)
## Summary
Fix all app mode widgets (including seed) disappearing after hard
refresh due to a race condition in `pruneLinearData` and a missing
reactivity dependency in `mappedSelections`.
## Changes
- **What**: Guard `pruneLinearData` with `!ChangeTracker.isLoadingGraph`
so inputs are preserved while `rootGraph.configure()` hasn't populated
nodes yet. Add `graphNodes` dependency to `mappedSelections` computed in
`LinearControls.vue` so it re-evaluates when the graph finishes
configuring.
## Review Focus
The core fix is a one-line guard change: `app.rootGraph &&
!ChangeTracker.isLoadingGraph` instead of just `app.rootGraph`. The
previous guard failed because `rootGraph` exists as an empty graph
during loading — `resolveNode()` returns `undefined` for all nodes and
everything gets filtered out.
Fixes COM-16193
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9621-fix-app-mode-widgets-disappear-after-hard-refresh-31d6d73d3650811193f5e1bc8f3c15c8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- align the linear-mode `DropZone` upload indicator with the Figma file
upload states
- add a co-located Storybook story for the default and hover variants
- add a `forceHovered` preview prop so Storybook can render the hover
state deterministically
## Validation
- `pnpm typecheck` (run in the original workspace with dependencies
installed)
- `pnpm lint` (passes with one pre-existing warning in
`src/lib/litegraph/src/ContextMenu.ts`)
- Storybook smoke check is currently blocked by an existing workspace
issue: `vite-plugin-inspect` fails with `Can not found environment
context for client`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9690-feat-add-DropZone-Storybook-coverage-for-file-upload-states-31f6d73d365081ae9eabdde6b5915f26)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Rename the `Comfy.Queue.QPOV2` settings label to `Docked job
history/queue panel` to improve searchability/discoverability in the
settings UI.
## Changes
- **What**: Updated the visible setting name in the core settings
definition and the English locale string.
## Review Focus
The change is intentionally limited to the display label. The persisted
setting key remains `Comfy.Queue.QPOV2`, so existing user configuration
is preserved.
## Screenshots (if applicable)
Not applicable.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9620-fix-rename-docked-queue-panel-setting-31d6d73d36508189a2d1d3a621739a22)
by [Unito](https://www.unito.io)
## Summary
This makes the focus ring only appear on keyboard navigation (Tab), not
on mouse click for widgets like toggle switches, while text inputs still
show the ring on click since browsers apply ` :focus-visible` to them.
## Mozilla Standard
Legacy `focus-within` triggers a highlight ring on every mouse click,
creating unnecessary visual noise during canvas navigation. Following
[MDN
standards](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible),
`:focus-visible` only triggers the highlight when the browser determines
a visual cue is needed (e.g., keyboard navigation).
Using the `:has()` relational selector allows the container to react to
the state of its children natively in CSS. Removes the need for Vue
event listeners or complex state bubbling to highlight the field border.
This reduces JavaScript overhead and simplifies component logic. FYI
[MDN
:has()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has).
Reordered Tailwind classes to move `transition-all` to the end,
following official best practices. Groups layout/shape first, followed
by interaction states, and finally animations. This improves code
readability and maintainability.
## Screenshots
before
<img width="558" height="252" alt="12efd5721fb792a7e2dab7e022c2bed6"
src="https://github.com/user-attachments/assets/f881fe13-9f4f-40fd-a8cc-f438b1ba4bde"
/>
after
<img width="538" height="235" alt="e5ffec0a34d3b237c4fca9818ec598dd"
src="https://github.com/user-attachments/assets/5ada4112-64bd-48a4-9e9c-b59de6984370"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9456-fix-update-WidgetLayoutField-border-styling-31b6d73d36508193a31ed02bfdef414f)
by [Unito](https://www.unito.io)
Co-authored-by: Terry Jia <terryjia88@gmail.com>
## Summary
Show tab-specific empty state messages instead of a misleading registry
connection error on tabs that don't depend on the Manager API.
## Changes
- **What**: Scoped `comfyManagerStore.error` to only affect tabs that
actually use the Manager API (AllInstalled, UpdateAvailable,
Conflicting). Missing and Workflow tabs use the registry directly, so
the Manager API error is irrelevant to them. Previously, any prior
Manager API failure would cause `"Error connecting to the Comfy Node
Registry"` to appear on the Missing tab even when there are simply no
missing nodes.
## Review Focus
The `isManagerErrorRelevant` computed only shows the error for Manager
API-dependent tabs. Verify that AllInstalled/UpdateAvailable/Conflicting
still correctly show the error when Manager API fails.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9640-fix-show-correct-empty-state-on-Missing-tab-instead-of-misleading-registry-error-31e6d73d365081868da1d226a9bb5fdc)
by [Unito](https://www.unito.io)
## Summary
Image input changes (dropdown selection and file upload) in app/linear
mode did not create their own undo entries, causing undo to skip or
bundle image changes with subsequent actions.
## Changes
- **What**: Add explicit `checkState()` calls in
`WidgetSelectDropdown.vue` after `modelValue` is set in
`updateSelectedItems()` (dropdown selection) and `handleFilesUpdate()`
(file upload), ensuring each image change gets its own undo entry.
## Review Focus
The fix is intentionally scoped to `WidgetSelectDropdown` rather than
the generic `updateHandler` in `NodeWidgets.vue`, which would create
excessive undo entries for text inputs. The pattern follows existing
usage in `useSelectedNodeActions.ts` and other composables.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9623-fix-call-checkState-after-image-input-changes-for-proper-undo-tracking-31d6d73d3650814781dbca5db459ab6d)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Restores `widget.inputEl` assignment on STRING multiline widgets that
was removed in commit a7c211516 (PR #8594) when it was renamed to
`widget.element`. Custom nodes (e.g. comfyui-custom-scripts) rely on
`widget.inputEl` to call `addEventListener` or set `readOnly`.
- FixesComfy-Org/ComfyUI#12893
## Test plan
- Verify custom nodes that access `widget.inputEl` on STRING widgets
work correctly
- Verify `widget.element` still works as before
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>