## Summary
Make the CodeRabbit end-to-end regression coverage check actually block
fix-like PRs until it is resolved or explicitly overridden by a
requested reviewer, and harden the prompt so it evaluates only PR-local
metadata.
## Changes
- **What**: Set the `End-to-end regression coverage for fixes` custom
check mode from `warning` to `error`
- **What**: Enable `reviews.request_changes_workflow` so CodeRabbit can
block on failed `error` pre-merge checks
- **What**: Set
`reviews.pre_merge_checks.override_requested_reviewers_only` to `true`
so only requested reviewers can bypass a failed check
- **What**: Tighten the custom check instructions to use only PR
metadata in review context, avoid shell commands, and avoid reverse-diff
or base-branch file evaluation
## Review Focus
Confirm this is the intended CodeRabbit enforcement model for missing
Playwright regression coverage on fix-like PRs and that the prompt
wording is strict enough to avoid false positives from reversed diffs.
## Summary
Fix tree explorer nodes not filling the full width of the sidebar
container, causing text to overflow instead of truncating.
## Changes
- **What**: Add `min-w-0` to `TreeRoot` to allow flex shrinking within
sidebar. Add `w-full` and `min-w-0` to tree node rows so
absolutely-positioned virtualizer items fill the container width and
text truncates correctly.
<img width="365" height="749" alt="image"
src="https://github.com/user-attachments/assets/320910f3-52ad-4634-a935-6bd1a40aea7f"
/>
## Review Focus
The virtualizer renders each item with `position: absolute; left: 0` but
no explicit width, so rows would size to content rather than filling the
container. Adding `w-full` ensures rows stretch to 100% of the
virtualizer container, and `min-w-0` allows proper flex shrinking for
deep indentation levels.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9964-fix-tree-explorer-nodes-not-filling-parent-container-width-3246d73d36508138be38fdcac15ae4ef)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Adds Total Blocking Time (TBT) and frame duration metrics to the
performance testing infrastructure, plus three new test scenarios
covering zoom, pan, and many-nodes-idle.
## Changes
### New Metrics
- **`totalBlockingTimeMs`** — Computed from PerformanceObserver
`longtask` entries: `sum(duration - 50ms)` for tasks >50ms. Measures
main thread blocking.
- **`frameDurationMs`** — Average frame duration via rAF timing (16.67ms
= 60fps target). Measures rendering smoothness.
### New Test Scenarios
| Scenario | Description |
|---|---|
| `canvas-zoom-sweep` | 10 zoom-in + 10 zoom-out cycles on default
workflow |
| `canvas-pan-many-nodes` | 10 pan sweeps over 100-node workflow |
| `canvas-many-nodes-idle` | 2-second idle measurement with 100 nodes
rendered |
### Infrastructure
- `PerformanceHelper.ts`: Installs PerformanceObserver for longtask,
collects TBT, measures frame duration via rAF
- `perf-report.ts`: Reports TBT and frame duration in PR comment tables
- `browser_tests/assets/perf/many_nodes_100.json`: 100-node (10×10 grid)
test fixture
## Review Focus
- TBT collection clears entries at `startMeasuring()` and reads at
`stopMeasuring()` — ensure no race with observer buffering
- Frame duration sampling uses 10 frames — enough for signal without
slowing tests
Depends on: #9886, #9887
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9910-feat-add-TBT-frameDuration-metrics-and-new-perf-test-scenarios-3236d73d365081488ae3c594a8bf7cff)
by [Unito](https://www.unito.io)
## Summary
Fix /cloud/subscribe route hanging indefinitely because billing context
never initializes during the onboarding flow.
## Changes
- **What**: Replace passive `await until(isInitialized).toBe(true)` with
explicit `await initialize()` in CloudSubscriptionRedirectView. Remove
unused `until` import.

## Review Focus
In the onboarding flow, `useTeamWorkspaceStore().activeWorkspace` is not
set, so `useBillingContext`'s internal watch (which triggers
`initialize()` on workspace change) enters the `!newWorkspaceId` branch
— it resets `isInitialized` to `false` and returns without ever calling
`initialize()`. The old code then awaited `isInitialized` becoming
`true` forever.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9965-fix-cloud-subscribe-redirect-hangs-waiting-for-billing-init-3246d73d3650812d93ebd477c544fa0a)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## What
Adds a 245-node workflow asset and two `@perf` tests to establish a
baseline for large-graph performance regressions (Tier 6 in the
performance backlog).
## Why
Backlog item N5: we need CI regression detection for compositor layer
management, GPU texture count, and transform pane cost at 245+ nodes.
This is PR1 of 2 — establishes baseline metrics on main. Future
optimization PRs will show improvement deltas against this baseline.
## Tests Added
- **`large graph idle rendering`** — 120 frames idle with 245 nodes,
measures style recalcs, layouts, task duration, heap delta
- **`large graph pan interaction`** — middle-click pan across 245 nodes,
stresses compositor layer management and transform recalculation
## Workflow Asset
`browser_tests/assets/large-graph-workflow.json` — 245 nodes (49
pipelines of CheckpointLoader → 2× CLIPTextEncode → KSampler +
EmptyLatentImage), 294 links. Minimal structure focused on node count.
## Verification
- [x] `pnpm typecheck:browser` passes
- [x] `pnpm lint` passes (eslint on changed file)
- [x] All link references in JSON validated programmatically
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9940-test-add-large-graph-perf-test-with-245-node-workflow-backlog-N5-3246d73d365081f6b5d8ddb9a85e6ad0)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Add a Claude Code skill (`/red-green-fix`) that enforces the red-green
commit pattern for bug fixes
- Ensures a failing test is committed first (red CI), then the fix is
committed separately (green CI)
- Gives reviewers proof that the test actually catches the bug
- Includes `reference/testing-anti-patterns.md` with common mistakes
contextualized to this codebase
## Structure
```
.claude/skills/red-green-fix/
├── SKILL.md # Main skill definition
└── reference/
└── testing-anti-patterns.md # Anti-patterns guide
```
## Test Plan
- [ ] Invoke `/red-green-fix <bug description>` in Claude Code and
verify the two-step workflow
- [ ] Confirm PR template includes red-green verification table
- [ ] Review anti-patterns reference for completeness
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9954-draft-add-red-green-fix-skill-for-verified-bug-fix-workflow-3246d73d365081339a83dc09263b0f33)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Fixes three critical issues with the CI performance reporting pipeline
that made perf reports useless on PRs (demonstrated by PR #9248 — deep
watcher removal merged without useful perf signal).
## Changes
### 1. Fix z-score baseline variance collection (`0/5 runs`)
**Root cause:** PR #9305 added z-score statistical analysis code to
`perf-report.ts`, but the historical data download step was placed in
the wrong workflow file. The report is generated in
`pr-perf-report.yaml` (a `workflow_run`-triggered job), but the
historical download was in `ci-perf-report.yaml` (the test runner) —
different runners, different filesystems.
**Fix:** Implement `perf-data` orphan branch storage:
- On push to main: save `perf-metrics.json` to `perf-data` branch with
timestamped filename
- On PR report: fetch last 5 baselines from `perf-data` branch into
`temp/perf-history/`
- Rolling window of 20 baselines, oldest pruned automatically
- Same pattern used by `github-action-benchmark` (33.7k repos)
### 2. Fix force-push comment staleness
**Root cause:** `cancel-in-progress: true` kills the perf test run
before it uploads artifacts. The downstream report workflow only
triggers on `conclusion == 'success'` — cancelled runs are ignored, so
the comment from the first successful run goes stale.
**Fix:**
- Change `cancel-in-progress: false` — with GitHub's queue depth of 1,
rapid pushes (A,B,C,D) run A and D, skipping B and C
- Add SHA validation in `pr-perf-report.yaml` — before posting, check if
the workflow_run's head SHA still matches the PR's current head. Skip
posting stale results.
### 3. Add permissions for baseline operations
- `contents: write` on CI job (needed for pushing to perf-data branch)
- `actions: read` on both workflows (needed for artifact/baseline
access)
## One-time setup required
After merging, create the `perf-data` orphan branch:
```bash
git checkout --orphan perf-data
git rm -rf .
echo '# Performance Baselines' > README.md
mkdir -p baselines
git add README.md baselines
git commit -m 'Initialize perf-data branch'
git push origin perf-data
```
The first 2 pushes to main after setup will build up variance data, and
z-scores will start appearing in PR reports (threshold is
`historical.length >= 2`).
## Testing
- YAML validated with `yaml.safe_load()`
- `perf-report.ts` `loadHistoricalReports()` already reads from
`temp/perf-history/<index>/perf-metrics.json` — no code changes needed
- All new steps use `continue-on-error: true` for graceful degradation
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9886-fix-fix-perf-CI-pipeline-z-score-baselines-force-push-staleness-baseline-storage-3226d73d365081538424c7945e71f308)
by [Unito](https://www.unito.io)
## What
Makes Vue nodes (Nodes 2.0) the default renderer for new desktop app
installs (version ≥1.41.0), matching the behavior already live for cloud
new installs.
## Why
Step 2 of the Nodes 2.0 rollout sequence:
1. ✅ Cloud new installs (≥1.41.0) — DONE
2. 👉 **Desktop app (new installs)** — this PR
3. ⬜ Local installs
4. ⬜ Remove Beta tag
5. ⬜ GTM announcement
No forced migration — only changes the default for new installs.
Existing users keep their setting. Rollback is a settings flip.
## Change
In `coreSettings.ts`, the `defaultsByInstallVersion` for
`Comfy.VueNodes.Enabled` changes from:
```typescript
defaultsByInstallVersion: { '1.41.0': isCloud },
```
to:
```typescript
defaultsByInstallVersion: { '1.41.0': isCloud || isDesktop },
```
## Gated on
- M2 perf target (≥52 FPS on 245-node workflow) — layer merge landed,
likely met
- M-DevRel migration docs (blocks Beta tag removal, not this flip)
Draft PR — ceremonial, to be merged when M2 checkpoint passes.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9947-feat-make-Vue-nodes-Nodes-2-0-default-for-new-desktop-installs-3246d73d365081b280dfff932c7aa016)
by [Unito](https://www.unito.io)
## Summary
Fix "Calculating dimensions" text flickering during live sampling
preview in Vue renderer.
## Changes
- **What**: Stop resetting `actualDimensions` to `null` on every
`imageUrl` change. Previous dimensions are retained while the new frame
loads, eliminating the flicker. Error state is still reset correctly.
## Review Focus
The watcher on `props.imageUrl` previously reset both `actualDimensions`
and `imageError`. Now it only resets `imageError`, since
`handleImageLoad` updates dimensions when the new frame actually loads.
This means stale dimensions show briefly between frames, which is
intentionally better than showing "Calculating dimensions" text.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9937-fix-prevent-live-preview-dimension-flicker-between-frames-3246d73d36508154a676e5996112354f)
by [Unito](https://www.unito.io)
## Summary
Expands the performance testing infrastructure to collect 4 additional
CDP metrics that are already returned by `Performance.getMetrics` but
were not being read. This is a zero-cost expansion — no additional CDP
calls, just reading more fields from the existing response.
## New Metrics
| Metric | CDP Source | What It Detects |
|---|---|---|
| `domNodes` | `Nodes` | DOM node count delta — widget DOM leaks during
node create/destroy |
| `jsHeapTotalBytes` | `JSHeapTotalSize` | Total heap delta — combined
with `heapDeltaBytes` shows GC pressure |
| `scriptDurationMs` | `ScriptDuration` | JS execution time vs total
task time — script vs rendering balance |
| `eventListeners` | `JSEventListeners` | Listener count delta — detects
listener accumulation across lifecycle |
## Changes
### `browser_tests/fixtures/helpers/PerformanceHelper.ts`
- Added 4 fields to `PerfSnapshot` interface
- Added 4 fields to `PerfMeasurement` interface
- Wired through `getSnapshot()` and `stopMeasuring()`
### `scripts/perf-report.ts`
- Added 4 fields to `PerfMeasurement` interface
- Expanded `MetricKey` type and `REPORTED_METRICS` array with 3 new
reported metrics (`domNodes`, `scriptDurationMs`, `eventListeners`)
- `jsHeapTotalBytes` is collected but not in `REPORTED_METRICS` — it's
used alongside `heapDeltaBytes` for GC pressure ratio analysis
## Why These 4
From a gap analysis of all ~30 CDP metrics, these were identified as
highest priority for ComfyUI:
- **`Nodes`** (P0): ComfyUI dynamically creates/destroys widget DOM. DOM
bloat from leaked widgets is a key performance risk, especially for Vue
Nodes 2.0.
- **`ScriptDuration`** (P1): Separates JS execution from layout/paint.
Reveals whether perf issues are script-heavy or rendering-heavy.
- **`JSEventListeners`** (P1): Widget lifecycle can leak listeners
across node add/remove cycles.
- **`JSHeapTotalSize`** (P1): With `JSHeapUsedSize`, the ratio shows GC
fragmentation pressure.
## Backward Compatibility
The `PerfMeasurement` interface is extended (not changed). Old baseline
`perf-metrics.json` files without these fields will have `undefined`
values, which the report script handles gracefully (shows `—` for
missing data).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9887-feat-expand-CDP-perf-metrics-add-DOM-nodes-script-duration-event-listeners-3226d73d3650818abea1d4a441667c38)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Add E2E test coverage for the textarea widget right-click context menu
inside subgraphs.
The fix was shipped in #9840 — this PR adds the missing browser test.
## Test
- Loads a subgraph workflow with a CLIPTextEncode (textarea) node
- Navigates into the subgraph
- Right-clicks the textarea DOM element
- Asserts that the ComfyUI "Promote Widget" context menu option appears
## Related
- Fixes the test gap from #9840
- Notion ticket: d7a53160-e1e1-42bb-a5ac-c0c2702c629c
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9891-test-add-browser-test-for-textarea-right-click-context-menu-in-subgraph-3226d73d365081a4be51f89b5d505361)
by [Unito](https://www.unito.io)
## What
Add equality check before updating `appScalePercentage` reactive ref.
## Why
Firefox profiler shows 586 `setElementText` markers from continuous text
interpolation updates during zoom/pan. The rounded percentage value
often doesn't change between events.
## How
Extract `updateAppScalePercentage()` helper with equality guard —
compares new rounded value to current before assigning to the ref.
## Perf Impact
Expected: eliminates ~90% of `setElementText` markers during zoom/pan
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9403-fix-skip-redundant-appScalePercentage-updates-during-zoom-pan-31a6d73d3650812db8f2d68ac73c95b0)
by [Unito](https://www.unito.io)
## Summary
Fixes text field becoming non-editable when a previously linked input is
removed from a custom node.
## Problem
When a widget's input was promoted to a slot, connected via a link, and
then the input was removed (e.g., by updating the custom node
definition), the widget retained stale `slotMetadata` with `linked:
true`. This prevented the widget from being editable.
## Solution
In `refreshNodeSlots`, removed the `if (slotInfo)` guard so
`widget.slotMetadata` is always assigned — either to valid metadata or
`undefined`. This ensures stale linked state is cleared when inputs no
longer match widgets.
## Acceptance Criteria
1. Text field remains editable after promote→connect→disconnect cycle
2. Text field returns to editable state when noodle disconnected
3. No mode switching needed to restore editability
## Testing
- Added regression test: "clears stale slotMetadata when input no longer
matches widget"
- All existing tests pass (18/18 in affected file)
---
**Note: This PR currently contains only the RED (failing test) commit
for TDD verification. The GREEN (fix) commit will be pushed after CI
confirms the test failure.**
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9885-fix-clear-stale-widget-slotMetadata-on-link-disconnect-3226d73d365081269319c027b42d9f6b)
by [Unito](https://www.unito.io)
## Summary
- Remove Ctrl+C and Ctrl+V keybindings from the keybinding service
defaults so native browser copy/paste events fire
- This restores image paste into LoadImage nodes, which broke after
#9459
## Problem
PR #9459 moved Ctrl+C/V into the keybinding service, which calls
`event.preventDefault()` on keydown. This prevents the browser `paste`
event from firing, so `usePaste` (which detects images in the clipboard)
never runs. The `PasteFromClipboard` command only reads from
localStorage, completely bypassing image detection.
**Repro:** Copy a node → copy an image externally → try to paste the
image into a LoadImage node → gets old node data from localStorage
instead.
## Fix
Remove Ctrl+C and Ctrl+V from `CORE_KEYBINDINGS` in `defaults.ts`. The
native browser events now fire as before, and `useCopy`/`usePaste`
handle them correctly. Ctrl+Shift+V, Ctrl+A, Delete, and Backspace
keybindings remain in the keybinding service.
Fixes#9459 (regression)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9914-fix-restore-native-copy-paste-events-for-image-paste-support-3236d73d365081c7ac53f983f316e10f)
by [Unito](https://www.unito.io)
## Summary
Hide the template selector when a first-time cloud user accepts a shared
workflow from a share link, so the shared workflow opens without the
onboarding template dialog lingering.
## Changes
- **What**: Added shared-workflow loader behavior to close the global
template selector on accept actions (`copy-and-open` and `open-only`)
while keeping cancel behavior unchanged.
- **What**: Added targeted unit tests covering hide-on-accept and
no-hide-on-cancel behavior in the shared workflow URL loader.
## Review Focus
Confirm that share-link accept paths now dismiss the template selector
and that cancel still leaves it available.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9913-fix-hide-template-selector-after-shared-workflow-accept-3236d73d365081099c04e350d499fad2)
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)
## Summary
- Add `quickRegister()` entries for 3 model directories missing UI
backlinks:
- `CogVideo` → `DownloadAndLoadCogVideoModel` (covers CogVideo,
CogVideo/ControlNet/*, CogVideo/VAE)
- `inpaint` → `INPAINT_LoadInpaintModel`
- `layer_model` → `LayeredDiffusionApply`
These mappings ensure the "Use" button in the model browser correctly
creates
the appropriate loader node when users click on models from these node
packs.
## Test plan
- [ ] Verify "Use" button works for CogVideo models in model browser
- [ ] Verify "Use" button works for inpaint models (fooocus, lama, MAT)
- [ ] Verify "Use" button works for LayerDiffuse models
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9890-feat-add-model-to-node-mappings-for-CogVideo-inpaint-and-LayerDiffuse-3226d73d3650816ea547fbcdf3a20c35)
by [Unito](https://www.unito.io)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replacement for https://github.com/Comfy-Org/ComfyUI_frontend/pull/9364
## Summary
Add a generic mechanism for widgets to reactively display upstream
linked values when disabled, with concrete implementations for curve and
imagecrop widgets.
## Changes
- What: When a widget input is linked to an upstream node, the widget
enters a disabled state and displays the upstream node's current value
reactively. This is built as a two-layer system:
- Infrastructure layer: Resolve link origin info (originNodeId,
originOutputName) in buildSlotMetadata, pass it through
SimplifiedWidget.linkedUpstream, and provide a generic useUpstreamValue
composable that reads upstream values from widgetValueStore.
- Widget layer: Each widget type provides its own ValueExtractor to
interpret upstream data. singleValueExtractor handles simple
type-matched values (e.g. CurvePoint[]); boundsExtractor composes a
Bounds object from either a single upstream widget or four individual
x/y/width/height number widgets.
- Curve widget: shows upstream curve points in read-only mode
- ImageCrop widget: shows upstream bounding box with disabled crop
handles and number inputs
- CurveEditor and WidgetBoundingBox: gain disabled prop support
## Adapting future widgets
The system is designed so that any widget needing upstream value display
only needs to:
1. Accept widget: SimplifiedWidget as a prop (provides linkedUpstream
automatically)
2. Call useUpstreamValue(() => widget.linkedUpstream, extractor) with a
suitable extractor
3. Use singleValueExtractor(typeGuard) for single-value types, or write
a custom ValueExtractor for composite cases like boundsExtractor
4. Compute an effectiveValue that switches between upstream and local
based on disabled state
No infrastructure changes are needed — linkedUpstream is already
populated for all widget types that have a corresponding input slot.
## Review Focus
- The buildSlotMetadata helper is shared between extractVueNodeData and
refreshNodeSlots — verify the graph ref is reliably available in both
paths
- boundsExtractor composing from 4 individual number widgets
(x/y/width/height) — this handles the BBox→ImageCrop case where upstream
exposes separate widgets rather than a single Bounds object
## Screenshots
https://github.com/user-attachments/assets/dbc57a44-c5df-44f0-acce-d347797ee8fb
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9851-feat-reactive-upstream-value-display-for-disabled-curve-and-imagecrop-widgets-3226d73d36508134b386ddc9b9f1266b)
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
Previously when I switch from nodes 1.0 to 2.0, positions and sizes of
nodes do not follow 'always snap to grid'. You can guess what a mess it
is for people relying on snap to grid to retain sanity. This PR fixes
it.
## Changes
In `ensureCorrectLayoutScale`, we call `snapPoint` after the position
and the size are updated.
We also need to ensure that the snapped size is larger than the minimal
size required by the content, so I've added 'ceil' mode to `snapPoint`,
and the patch is larger than I thought first.
I'd happily try out nodes 2.0 once this is addressed :)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9332-fix-respect-always-snap-to-grid-when-auto-scale-layout-from-nodes-1-0-to-2-0-3176d73d365081f5b6bcc035a8ffa648)
by [Unito](https://www.unito.io)
## Summary
Adds `view_mode` and `is_app_mode` properties to the
`app:run_button_click` telemetry event so analytics can segment run
button clicks by the user's current view context.
## Changes
- **`types.ts`**: Added `view_mode?: string` and `is_app_mode?: boolean`
to `RunButtonProperties`
- **`PostHogTelemetryProvider.ts`**: Computes `view_mode` and
`is_app_mode` from `useAppMode()` in `trackRunButton()`
- **`MixpanelTelemetryProvider.ts`**: Same as PostHog (providers are
mirrors)
## New Properties
| Property | Type | Description | Example Values |
|----------|------|-------------|----------------|
| `view_mode` | `string` | Granular AppMode value | `'graph'`, `'app'`,
`'builder:inputs'`, `'builder:outputs'`, `'builder:arrange'` |
| `is_app_mode` | `boolean` | Simplified flag for app mode vs non-app
mode | `true` when `mode === 'app' \|\| mode === 'builder:arrange'` |
## Design Decisions
- **Both granular and simplified**: `view_mode` gives exact mode for
detailed analysis; `is_app_mode` gives a quick boolean for simple
segmentation
- **Computed in providers**: View mode is read from `useAppMode()` at
tracking time, same pattern as `getExecutionContext()` — no changes
needed at call sites
- **`trigger_source` unchanged**: `keybindingService.ts` already reports
`trigger_source: 'keybinding'` regardless of view mode, satisfying the
requirement that keybinding invocations are correctly identified even in
app mode
## Testing
- Typecheck passes (no new errors)
- Format and lint pass (no new errors)
- Manual verification: all pre-existing errors are in unrelated files
(`draftCacheV2.property.test.ts`, `workflowDraftStoreV2.fsm.test.ts`)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9881-feat-telemetry-add-view_mode-and-is_app_mode-to-run_button_click-event-3226d73d36508101b3a8c7ff27706f81)
by [Unito](https://www.unito.io)
## Summary
Add structured preload error logging with Sentry context enrichment and
a user-facing toast notification when chunk loading fails (e.g. after a
deploy with new hashed filenames).
## Changes
- **`parsePreloadError` utility** (`src/utils/preloadErrorUtil.ts`):
Extracts structured info from `vite:preloadError` events — URL, file
type (JS/CSS/unknown), chunk name, and whether it looks like a hash
mismatch.
- **Sentry enrichment** (`src/App.vue`): Sets Sentry context and tags on
preload errors so they are searchable/filterable in the Sentry
dashboard.
- **User-facing toast**: Shows an actionable "please refresh" message
when a preload error occurs, across all distributions (cloud, desktop,
localhost).
- **Capture-phase resource error listener** (`src/App.vue`): Catches
CSS/script load failures that bypass `vite:preloadError` and reports
them to Sentry with the same structured context.
- **Unit tests** (`src/utils/preloadErrorUtil.test.ts`): 9 tests
covering URL parsing, chunk name extraction, hash mismatch detection,
and edge cases.
## Files Changed
| File | What |
|------|------|
| `src/App.vue` | Preload error handler + resource error listener |
| `src/locales/en/main.json` | Toast message string |
| `src/utils/preloadErrorUtil.ts` | `parsePreloadError()` utility |
| `src/utils/preloadErrorUtil.test.ts` | Unit tests |
## Review Focus
- Toast fires for all distributions (cloud/desktop/localhost) —
intentional so all users see stale chunk errors
- `parsePreloadError` is defensive — returns `unknown` for any field it
cannot parse
- Capture-phase listener filters to only `<script>` and `<link
rel="stylesheet">` elements
## References
- [Vite preload error
handling](https://vite.dev/guide/build#load-error-handling)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Custom names set on subgraph output nodes are ignored in the v2 renderer
— it always shows the data type name (e.g. "texts") instead of the
user-defined label. Works correctly in v1.
## Changes
- **What**: Made `outputs` in `extractVueNodeData` reactive via
`shallowReactive` + `defineProperty` (matching the existing `inputs`
pattern). Added a `node:slot-label:changed` graph trigger that
`SubgraphNode` fires when input/output labels are renamed, so the Vue
layer picks up the change.
## Review Focus
- The `outputs` reactivity mirrors `inputs` exactly — same
`shallowReactive` + setter pattern. The new trigger event forces
`shallowReactive` to detect the deep property change by re-assigning the
array.
- Also handles input label renames for consistency, even though the
current bug report is output-specific.
## Screenshots
**v1 — output correctly shows custom label "output_text":**
<img width="1076" height="628" alt="Screenshot 2026-02-26 at 4 43 00 PM"
src="https://github.com/user-attachments/assets/b4d6ae4c-9970-4d99-a872-4ce1b28522f2"
/>
**v2 before fix — output shows type name "texts" instead of custom
label:**
<img width="808" height="298" alt="Screenshot 2026-02-26 at 4 43 30 PM"
src="https://github.com/user-attachments/assets/cf06aa6c-6d4d-4be9-9bcd-dcc072ed1907"
/>
**v2 after fix — output correctly shows "output_text":**
<img width="1013" height="292" alt="Screenshot 2026-02-26 at 5 14 44 PM"
src="https://github.com/user-attachments/assets/3c43fa9b-0615-4758-bee6-be3481168675"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9266-fix-subgraph-output-slot-labels-not-updating-in-v2-renderer-3146d73d365081979327fd775a6ef62b)
by [Unito](https://www.unito.io)
## Summary
Fix node layout drift from repeated `ensureCorrectLayoutScale` scaling,
simplify it to a pure one-time normalizer, and fix links not following
Vue nodes during drag.
## Changes
- **What**:
- `ensureCorrectLayoutScale` simplified to a one-time normalizer:
unprojects legacy Vue-scaled coordinates back to canonical LiteGraph
coordinates, marks the graph as corrected, and does nothing else. No
longer touches the layout store, syncs reroutes, or changes canvas
scale.
- Removed no-op calls from `useVueNodeLifecycle.ts` (a renderer version
string was passed where an `LGraph` was expected).
- `layoutStore.finalizeOperation` now calls `notifyChange` synchronously
instead of via `setTimeout`. This ensures `useLayoutSync`'s `onChange`
callback pushes positions to LiteGraph `node.pos` and calls
`canvas.setDirty()` within the same RAF frame as a drag update, fixing
links not following Vue nodes during drag.
- **Tests**: Added tests for `ensureCorrectLayoutScale` (idempotency,
round-trip, unknown-renderer no-op) and `graphRenderTransform`
(project/unproject round-trips, anchor caching).
## Review Focus
- The `setTimeout(() => this.notifyChange(change), 0)` →
`this.notifyChange(change)` change in `layoutStore.ts` is the key fix
for the drag-link-sync bug. The listener (`useLayoutSync`) only writes
to LiteGraph, not back to the layout store, so synchronous notification
is safe.
- `ensureCorrectLayoutScale` no longer has any side effects beyond
normalizing coordinates and setting `workflowRendererVersion` metadata.
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Hunter <huntcsg@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Fix duplicate LLink objects created during subgraph unpacking, where
output.links contains multiple link IDs for the same connection but
input.link only references one, leaving orphaned links.
## Changes
- **What**: Three layers of defense against duplicate links:
1. **Serialization fix** (`slotUtils.ts`): Clone `output.links` array in
`outputAsSerialisable` to prevent shared-reference mutation during
serialization round-trips
2. **Self-healing** (`LGraph.ts`): `_removeDuplicateLinks()` sanitizes
corrupted data during `configure()`, keeping the link referenced by
`input.link` and removing orphaned duplicates from `output.links` and
`_links`
3. **Unpack dedup** (`LGraph.ts`): Subgraph unpacking filters `newLinks`
via a `seenLinks` Set before creating connections
Runtime diagnostic logging via `graph.events` (no Sentry import in
litegraph):
- `_dupLinkIndex` Map for O(1) duplicate detection, only allocated when
enabled
- `_checkDuplicateLink()` called at the 3 link-creation sites
(`connectSlots`, `SubgraphInput.connect`, `SubgraphOutput.connect`)
- App layer listens for `diagnostic:duplicate-link` events and forwards
to Sentry with rate-limiting (1 per key per 60s)
## Review Focus
- The `_removeDuplicateLinks` strategy of keeping the link referenced by
`input.link` and removing others from `output.links` + `_links`
- The diagnostic index lifecycle: built on enable, updated on link
create/remove, cleared on disable
- Sentry integration in `app.ts` using the existing `graph.events`
system to avoid coupling litegraph to Sentry
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9120-fix-detect-and-remove-duplicate-links-in-subgraph-unpacking-3106d73d3650815b995ddf8f41da67ae)
by [Unito](https://www.unito.io)
## What
Replace `canvas.offsetHeight` with `canvas.height / devicePixelRatio` in
`renderInfo` to avoid forced synchronous layout.
## Why
`renderInfo` is called ~2,631 times in a typical session. Each call
reads `this.canvas.offsetHeight`, which forces the browser to flush
pending style/layout changes synchronously. With PrimeVue injecting
styles dynamically and Vue patching the DOM, there are almost always
pending mutations — converting every canvas-only `renderInfo` call into
a forced layout.
## How
`canvas.height` is the DPR-scaled internal resolution (set in
`resizeCanvas` as `cssHeight * devicePixelRatio`). Dividing by
`devicePixelRatio` yields the same CSS pixel value as `offsetHeight`
without triggering layout.
## Verification
- [x] Unit test: verifies `offsetHeight` is not accessed when y is
provided
- [x] Unit test: verifies fallback uses `canvas.height /
devicePixelRatio`
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All litegraph tests pass (538 passed)
## Perf Impact
Eliminates ~2,631 forced synchronous layouts per session from the canvas
info panel.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9304-fix-avoid-forced-layout-in-renderInfo-by-using-canvas-height-3156d73d36508171973dda289b30d5ee)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## What
Add a per-frame text measurement cache for all hot-path
ctx.measureText() calls.
## Why
drawTruncatingText() in BaseWidget calls ctx.measureText() per widget
per frame with zero caching. For a 50-node workflow at 60fps:
~78,000-243,000 measureText calls/sec. Text labels rarely change between
frames.
## How
Global Map<string, number> cache keyed by font+text, cleared once per
frame at the start of drawFrontCanvas(). Replaces direct
ctx.measureText() calls in BaseWidget.drawTruncatingText, draw.ts
truncateTextToWidth/drawTextInArea, LGraphBadge.getWidth,
LGraphButton.getWidth, and textUtils.truncateText.
## Perf Impact
Expected: ~95% reduction in measureText calls (only cache misses on
first frame and value changes). Firefox has slower measureText than
Chrome, so this disproportionately benefits Firefox.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9404-fix-cache-ctx-measureText-results-to-avoid-redundant-calls-in-draw-loop-31a6d73d3650814e9cdac16949c55cb7)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Add frontend support for `prompt_id` in `progress_text` binary WS
messages, enabling parallel workflow execution to route progress text to
the correct active prompt.
Backend PR: https://github.com/Comfy-Org/ComfyUI/pull/12540
## Changes
- Advertise `supports_progress_text_metadata` in client feature flags
- Decode `prompt_id` from new binary format when flag is active
- Add optional `prompt_id` to `zProgressTextWsMessage` schema
- Filter `progress_text` events by `activePromptId` — skip messages for
non-active prompts
## Deployment
Can be deployed independently in any order — feature flag negotiation
ensures graceful degradation.
Part of COM-12671
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9002-Add-prompt_id-support-to-progress_text-WS-messages-30d6d73d365081acabbcdac939e0c751)
by [Unito](https://www.unito.io)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Progress text updates can include optional metadata so richer context
is available.
* **Bug Fixes / Improvements**
* Progress updates are now filtered to show only the currently active
prompt, reducing cross-talk from concurrent operations and improving
update accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: GitHub Action <action@github.com>
## 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>
## 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>