Double-clicking a group row flips its enabled state via the rgthree
Fast Groups Muter (off->on, on->off), with the pill label and row
highlight updated immediately. No-op for placeholder rows with no muter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-digit hotkeys (1..9) jump immediately. With more than 9 groups,
typing a digit now buffers briefly so two-digit row numbers (10, 11,
12, ...) work too. Buffer commits on:
- 350ms idle (lets "1" still jump to row 1 after a short pause)
- any non-digit key (flushes pending buffer first)
- "dead end" digits where no longer prefix could match any row
(e.g. with 15 rows, pressing "2" commits instantly since 2x is
out of range — keeps single-digit feel snappy where possible)
Sits next to Disable All. Calls queuePromptSafely() — which tries
app.queuePrompt(0) first, then falls back to clicking the canvas-side
Run button if the API shape isn't available.
Distinct green styling (#2d7d46) so it doesn't blend with the blue
Enable/Disable buttons. Queues the prompt using whatever groups are
currently enabled, same as ComfyUI's own Run.
Lets users define a workflow sequence (step 1 → step 2 → ...) by
dragging rows in the panel. Order is independent of rgthree's
auto-sort and persists across reloads.
How it works:
- Each row gets draggable="true" + HTML5 DnD handlers.
- On drop, reorderGroup(moving, target, before|after) computes the new
ordering, writes it to localStorage (ORDER_KEY), and re-renders.
- Drop indicator: inset top/bottom box-shadow in the accent color
showing where the dragged row will land.
- sortByCustomOrder() applied at the end of getNavigableGroups so the
matchColors filtering still happens first.
- Groups not yet in the saved order keep relative order and end up
after listed ones. New groups auto-append on the next reorder write.
Shortcut badges follow the new order — 1..9 always map to the first 9
rows as they appear in the panel, so dragging a group to the top
assigns it shortcut "1" (assuming no custom override).
Settings panel gains two reset buttons: "Reset row order" (just the
order) and "Reset all settings" (colors + shortcuts + order).
whose contents overlap a previously-enabled group
rgthree's public widget.toggle(value) is misleadingly named — it doesn't
honor the passed value, it calls doModeChange() with no force argument,
and doModeChange then COMPUTES the next state from the current node
modes:
newValue = !hasAnyActiveNodes
So if any inner node is already ALWAYS (e.g. shared with a
previously-toggled overlapping/parent group), toggle(true) actually
DISABLES the group rather than enabling it. Symptom: Enable All flipped
13 of 14 groups; "Place an image" (whose 25 nodes overlapped with an
earlier-enabled neighbor) stayed at NO.
Fix: setMuterWidget now calls widget.doModeChange(target, true) directly
— the (force, skipOtherNodeCheck) path that actually forces the state
and skips rgthree's "max one"/"always one" restriction handling that
could re-disable peers. Falls back to the old toggle/value paths only
for non-rgthree widgets.
Two related fixes:
- New getShortcutKeyForGroup(title, index): looks up which key would
jump to this group. Inverse of resolveShortcut. Custom mappings
take precedence; defaults (1..9) only show when not overridden.
- renderList now uses that helper for the badge instead of hardcoding
String(index + 1). So a custom shortcut '0' → "Text to Image (Flux.2
Klein 9B)" now displays "0" on that row, and the slot that used to
show "1" loses its badge if "1" is reassigned elsewhere.
- persistShortcutsFromDom calls scheduleRender() so badges update live
as the user types in the settings panel.
Bumped to 0.3.6.
Regression introduced in v0.3.1: resolveShortcut() bailed out as soon
as ANY saved shortcut map existed. So adding one custom binding (say
'g' → "Pulid for Flux 1") silently killed the 1..9 defaults entirely —
every other key returned null.
Fix: only return the custom mapping when there's an explicit entry for
the pressed key. Otherwise fall through to the 1..9 default. Net effect:
- Empty map → all defaults active (already correct from v0.3.4)
- Map with {g: ...} → 'g' jumps to the custom target; 1-9 still default
- Map with {1: "Skin repair"} → '1' overrides default, 2-9 still default
Two related bugs in keyboard shortcuts after the user cleared their
custom mappings:
1) Empty saved map killed defaults. resolveShortcut() short-circuited
on any saved map (including {}) instead of falling through to the
1..9 defaults. Now: loadShortcuts() returns null for empty/missing/
malformed values, and saveShortcuts() actively removes the
localStorage entry when the map empties — so deleting every row in
the settings panel restores the defaults.
2) rgthree's Bookmark node listens on its own KEY_EVENT_SERVICE — if
the user's bookmark and a navigator default share a key (e.g. both
"1"), pressing it caused MY handler to jump first, then rgthree's
bookmark would jump again, overriding the visual.
Fix: onKeyDown now calls preventDefault + stopImmediatePropagation
+ stopPropagation when we claim a key. The window-capture handler
runs before bubble-phase or service-dispatch listeners, so rgthree
no longer gets a chance to override.
If the user WANTS the rgthree bookmark to win for a specific key,
they can remove that key from the navigator settings — resolveShortcut
returns null and we don't intercept.
Two issues:
1) Enabled rows now have a visual indicator beyond the toggle pill:
- Bold title text tinted toward the accent color
- Left edge stripe in the accent color (via inset box-shadow)
- Subtle gradient wash from the accent color into the row bg
All driven by --cnv-accent so it picks up the user's custom color.
Applied at render time AND on every periodic sync so external
toggles (via the canvas muter directly) update the panel too.
2) Shortcut edits weren't persisting. Root cause: I only attached a
"change" event listener to the shortcut list, and text inputs only
fire change on blur. So typing a key char then immediately picking a
group from the select could lose the unsaved key value when the
panel closed without the input ever blurring.
Fix: also attach an "input" listener — fires per keystroke for text
inputs and on selection for selects. Either path now persists.
Header now distinguishes click vs drag:
- mousedown → mouseup with <4px total movement = treated as a CLICK,
toggles collapse (same as clicking the arrow)
- More movement = actual drag, keeps existing behavior
- Threshold avoids accidental toggling during tiny mouse jitters
Also enlarged the collapse arrow button: bigger font, padded box with
border + background tint, easier to hit. Either gesture (header click
or arrow click) does the same thing now.
Click the ⚙ button in the toolbar → settings slide down between toolbar
and group list.
Colors (4 pickers, live preview, persist to localStorage):
- Body background (defaults to rgthree teal #0e283f)
- Header bar (#1e4f88)
- Accent / toggle ON (#2864ad)
- Row hover (#163657)
All panel styling now reads from CSS custom properties so the live
update is just panel.style.setProperty(key, value).
Keyboard shortcuts (overrides defaults):
- Add rows one at a time: type a key char, pick a group from a dropdown
- × button removes a row
- Empty list = defaults restore (1..9 → first 9 groups by order)
- Single-key only (a-z, 0-9). Modifier keys ignored (Ctrl/Meta/Alt).
Reset button wipes both localStorage entries and rebuilds settings UI
with defaults.
Refactored onKeyDown to go through resolveShortcut() so the same code
path handles defaults and user overrides.
Per user feedback:
- REMOVE filter / search input — wasn't useful for 15 groups
- REMOVE color dot — was confusing, looked like a status indicator
but did nothing
- REMOVE "Enable " prefix on row titles — just show the group name
- REMOVE → jump button per row — clicking the row body already jumps
- REMOVE ⋯ menu per row — its actions weren't pulling weight
- REMOVE right-click context menu — same reason
- ADD toolbar between header and list:
[Enable All] [Disable All] [⚙ Settings]
Enable All / Disable All flip every widget in every muter at once.
Settings opens an alert stub for now — actual color + shortcut
customization will land in v0.3.1.
Net result: each row is just `Group Name [N] [toggle pill]`.
Click toggle pill = flip. Click anywhere else on the row = jump.
Bumped to 0.3.0 (breaking UI change).
Bug: rows could be permanently stuck as placeholders (no click handler,
no toggle pill, no nothing) if the panel rendered BEFORE the rgthree
Fast Groups Muter's widgets were constructed. The poll interval only
re-rendered when graph._groups.length changed; once placeholders shipped,
they stayed even after widgets caught up.
Now the interval also tracks total muter-widget count and re-renders
when it changes. It also force-re-renders any time placeholders exist
AND any muter widget exists — covers the case where my matcher and the
muter both happen to start with the same widget count but different
matches.
Confirmed in autonomous browser test that widget-count-change triggers
the re-render path. Could not fully verify the rendered output flips
to real toggles because loadGraphData() in headless test environment
does NOT construct rgthree's muter widgets the same way a real workflow
load does (widgets stay empty in the test). In a real user session,
widgets ARE present, so the fix applies.
Bug: clicking the toggle pill in the panel would visually flip to 'yes'
for ~600ms then revert to 'no'. My code was setting widget.value.toggled
directly, but rgthree's FastGroupsToggleRowWidget has a separate
doModeChange() method that performs the real work — calling
changeModeOfNodes() on every node inside the group. Without that call,
rgthree's own update loop re-reads node modes (still ALWAYS — i.e.
'enabled'), recomputes group.rgthree_hasAnyActiveNode, and overwrites
widget.toggled back to its previous value.
Fix: call widget.toggle(value), the public method on the
FastGroupsToggleRowWidget class. That sets value AND triggers
doModeChange(). Kept the old value-set path as fallback for unknown
widget shapes.
After realizing the canvas-Floater approach can't escape canvas zoom
(pinned canvas nodes always scale with ds.scale, making them unusable
when panned out), pivoted back to this DOM-overlay panel. It naturally
renders at constant size regardless of zoom.
This commit just makes the panel LOOK like rgthree's muter so it doesn't
feel like a separate UI:
- Dark blue card with rgthree-style blue header bar
- Each row: color dot + "Enable {Group Name}" + toggle pill + → + ⋯
- Toggle pill is a real pill: rounded rectangle with "no"/"yes" label
and a glowing knob that pulses when on (matches rgthree muter visuals)
- Alternating row backgrounds for readability
- Toggle pill is bidirectionally wired to the corresponding rgthree
widget — flip in panel → muter widget toggles → panel pill animates
(and vice versa via the existing 600ms sync)
Bumped CNV_VERSION to 0.2.0.
Each row in the panel is now a real two-way toggle wired to the
corresponding widget on rgthree's Fast Groups Muter. Click the checkbox
in the panel → muter widget flips. Toggle the muter widget directly on
the canvas → panel checkbox catches up on the next poll (600ms). Save +
load works because all state still lives in rgthree.
Per row layout:
[checkbox] [color dot] Group Name [shortcut N] [→ jump] [⋯ menu]
Click body of row = jump. Explicit → button = same jump (visible
affordance). ⋯ opens the same context menu as right-click. Groups with
no managing muter render a dashed placeholder where the checkbox would
be (visual signal: "this group is unmanaged").
Kept: drag-to-reposition, collapse, search, matchColors filter (only
groups in the primary muter render), keyboard 1-9 = jump, version
marker, all rgthree integration (single-muter Solo, all-muter Reset).
Bumped to v0.1.0 since the row contract changed.
User found 'Solo' confusing (audio-mixer metaphor doesn't land).
- 'Solo & Run this group' → 'Run only this group'
- 'Solo only (mute others, don't run)' → 'Mute other groups'
Internal function names (soloGroup, soloGroupViaMuter) kept for code
brevity — they're never user-visible.
Previous label 'Solo run this group' was misleading — it only configured
the muter (turn off other groups in the same muter scope) but didn't
actually queue the prompt. User had to click Run separately, which made
the action feel broken.
Now two menu items:
- 'Solo & Run' — configures the muter, waits a tick for rgthree callbacks
to settle, then calls app.queuePrompt(0) (falls back to clicking the Run
button if the API shape changes). One click → solo + queue.
- 'Solo only (mute others, don't run)' — same as the old behavior for
cases where you want to inspect / tweak before running.
Reset stays as-is.
User color-codes sub-groups (e.g. gold 'Flux Klein Workflow' inside a
larger blue 'Place an image' workflow) and routes them through a separate
Fast Groups Muter via matchColors. The intent: sub-groups aren't
top-level navigation targets, they're implementation details of their
parent workflow.
New getNavigableGroups(): if any muter exists, pick the one with the most
managed widgets as the 'primary navigation muter' and show only its
groups in the panel. Sub-groups managed by smaller scoped muters get
filtered out. No muter present → show every group (previous behavior).
Header count now shows 'N / M' when filtering, with tooltip explaining
how many sub-groups were hidden. Keyboard 1-9 follows the same filtered
list.
Workflows often use multiple Fast Groups Muters with the matchColors
property, splitting groups into independent toggle scopes (e.g. one muter
for all blue-color groups, another for the gold "Flux Klein Workflow").
Previous Solo turned off widgets across ALL muters indiscriminately —
soloing a blue group would clobber the gold muter's state too, defeating
the user's intentional color-based partitioning.
Now: getMutersAndWidgets() returns per-muter widget maps. Solo finds the
single muter that owns the target group (via findMuterOwningGroup) and
toggles only within that muter's widget set. Other muters are left
exactly as the user set them. Reset still spans all muters (turn
everything on).
Real widget structure (from live diagnostic):
- name: "RGTHREE_TOGGLE_AND_NAV" (generic, not per-group)
- type: "custom" (not "toggle" or "RGTHREE_TOGGLE")
- label: "Enable {group_title}" — the only link from widget to group
- value: { toggled: bool } — an object, not a plain boolean
My previous filter rejected every widget (wrong type guard) and tried to
set .value = true (wrong shape). Result: solo/reset did nothing.
Fix:
- Parse "Enable {title}" out of widget.label to map widget → group title
- Mutate widget.value.toggled, preserving other keys, and call w.callback(value)
- Scan ALL muters on the graph (workflow may have 2+); aggregate their
widget maps and only dirty the muters we actually touched
- Fall back to per-node mode flag when a muter is present but its widget
set doesn't include the target group, in addition to the no-muter case
JS-only ComfyUI extension. Floating right-anchored panel that:
- Auto-lists every LGraph group in the current workflow
- Click row → animated pan+zoom to fit the group on screen
- Search box at the top filters by title
- Right-click row → Solo run / Center / Reset
- Solo uses rgthree's Fast Groups Muter (driven via widget callbacks)
when present; otherwise falls back to node.mode = NEVER on every node
outside the soloed group's bounds
- Keyboard 1-9 jumps to the first nine groups
- Panel is draggable; position + collapsed state persisted to localStorage
- Hidden when graph has < 2 groups
No Python node, no drawNode hooks, no view-filtering — only canvas pan/zoom
+ rgthree widget poking + node.mode flag setting. None of the fragile
internal hooks that comfyui-tabs depended on.