The A | B badge stayed in the middle of the picture while the split line
moved. Now A rides just left of the line and B just right of it, on the
picture each one names; with one picture showing, its label sits alone.
Both panels filled only from onExecuted. Going to another workflow tab and
back rebuilds the nodes and restores the tab's results in ComfyUI's store
without an onExecuted, so they said "Run to preview" over a result they
had just made, in both renderers. On onNodeOutputsUpdated (a tab's results
restored) an empty panel loads its node's stored record. The output key
helper moves to shared/prompt_scope.mjs for the three users.
Every workflow tab in a page shares one server connection, and ComfyUI
files a node's results by node id. A run from one tab that finished while
another tab was open landed on the open tab's node with the same id. On
Video Crop + Rotate + Pad -> Clip that was a second video under the
buttons that grew the node over the one below; Show Text and Seed also
saved the other workflow's text and seed into this one, and Save Video and
Compare replaced their own result.
- The three Crop + Rotate + Pad nodes stand down ComfyUI's image and video
previews (they send no picture of their own), in both renderers.
- suppressCoreVideoPreview sets hideOutputImages, so Nodes 2.0 no longer
draws Save Video's clip a second time under its player.
- shared/prompt_scope.mjs notes which graph queued each run (the queue
call is wrapped for the prompt id). For a result from another tab, the
AusBoss node here gets this tab's own stored result back, every pack
onExecuted handler returns early, and the result is handed to the owner
tab's AusBoss nodes when that tab is open again. Core nodes are left
alone.
Tests: prompt_scope (scope rules plus a policy check that every
onExecuted handler asks isForeignRun first) and core_preview (every
transform kind suppresses both previews; video suppression sets
hideOutputImages) fail on main and pass here.
Every card and DOM panel was added with options.serialize = false, which
only keeps a widget out of the queued prompt. The frontend saves each
widget whose own serialize is not false and hands saved values back by
position, so each card and panel saved an empty value of its own after the
node's real values, and an input appended to the node later opened holding
it (Stitch Inpaint's seam, Crop For Inpaint's keep_inside). Every DOM widget
now also sets widget.serialize = false, as the frontend's own previews do,
so new saves carry only real values.
Older saves still end in those empty values, and loading them now skips
the cards and panels. On every node that saves by position they come after
all real values (Save Image's card sits mid-list, but Save Image saves by
name), so nothing moves. On frontend 1.53.6 the examples (the 21 from
before the trim and the 7 now) and 14 older saved workflows queue
byte-identical prompts before and after, keep every value, and save and
reopen cleanly.
An input appended later still needs a fallback for those older saves.
tests/saved_widget_values.test.mjs checks that every DOM widget sets the
flag, that the examples keep their values with the panels skipped, and that
each input appended to a node whose older saves end in an empty value is
declared and, as a widget input, has a card resetUnknown fallback that
fires. tests/test_card_fallbacks.py (ComfyUI's Python) checks each fallback
is the input's own default. AGENTS.md states the rule.
The pack's DOM panels must tell ComfyUI's change tracker about their
writes, and several did not: LoRA Loader row edits other than a few
menu actions, Compare's slide / A-B mode, and Mask Refine's AUTO. LoRA
rows now record on every commit, with strength drags recording once on
release, and the other two record when they change a saved value.
- Drag-to-reorder actually drops: the row preview's reparenting silently
released pointer capture, so the gesture now listens on the window for
its whole lifetime.
- Step arrows on every strength box (configured step; Shift = 0.01), with
range tinting carried over.
- The control bar (templates, stack toggle, count, settings) sits directly
on top of the row stack and + Add LoRA pins to the node's bottom edge;
the triggers output got its narrower name (slot links reconnect by index).
- Civitai lookup reads the whole response instead of the first TCP chunk,
so successful lookups stop dying as 'lookup failed'.
- Compare and Load Video declare their height floors WITH the DOM-widget
frame allowance, so minimum-height nodes stop shaving the caption and
trim strip; panel_layout documents the rule.
- README and the LoRA doc catch up; panel_guards comments follow the
body-as-layout-box reality.
Image Compare A/B drew a chip over the top-left of the picture carrying the
resolution and a hint about how to use the panel - covering part of what you
came to look at in order to say something you need once. The resolution moved
to a caption centred under the stage, and when the two sides are different
sizes both are named there: the panel scales them to fit, so nothing else on
screen would reveal the mismatch. The empty and error states still take the
middle of the stage, where there is no picture to hide.
HOLD became an A/B toggle. Press-and-hold meant the comparison only existed
while a mouse button was down, so the two images could never be held side by
side in memory. Each click now swaps the panel and the button names the side
you are looking at; flicking between them in place is how a small difference
actually becomes visible. A workflow saved on "hold" opens on the toggle it
became rather than quietly dropping back to slide.
The example workflow was stale in ways that would have shipped to Civitai:
its overview credited SimpleWatermarkRemover - the deprecated compatibility
id, not the node the graph actually uses - still called the node Refine Mask,
and linked to github.com/auboss, which does not exist. It gained model-setup
and requirements sections, consistent node titles, and the mask settings its
single-frame test was missing: that path shipped with every Mask Refine
control at zero, so the frame it previewed was not the result the full run
would produce, which is the one thing that test exists to show.
Dragging a node taller only ever widened its stage. The frontend
arranges widgets in one pass:
if (w.computeSize) -> fixed height, kept OUT of the split
else if (w.computeLayoutSize) -> joins distributeSpace(freeSpace, ...)
else -> one standard widget row
It is an else-if, so a widget declaring both is pinned by computeSize
and its computeLayoutSize is never called. Every stage, player and
filmstrip here derived that fixed height from the node's WIDTH, so the
extra height became dead space underneath. Compare was fixed by hand;
the other six had the same defect.
Adds js/shared/panel_layout.mjs with one fillNodeHeight() that deletes
computeSize and declares a floor with no ceiling - distributeSpace reads
a missing maxSize as Infinity, so the panel takes whatever is left.
Applied to Compare, Save Video, Load Video, Frame Chooser, Load Image +
Pad, Image Crop+Rotate+Pad, and the shared transform panel that Video
Crop+Rotate+Pad mounts. Removing computeSize also removed where those
nodes got their opening height, so Save Video, Load Video and Load Image
+ Pad now state a default; a saved size still wins through onConfigure.
Measured in the browser: all seven grow 1:1 with the node and clamp at
their own floor.
The panel audit now enforces it - a DOM panel is either a stage that
calls fillNodeHeight and never assigns computeSize, or is listed as
fixed by design - so a new panel cannot quietly reintroduce this.
Six fixes found while testing every node against real workflows.
LoRA Loader: the "wrong base model" warning never fired on a modern stack.
It matched comfy's model class name against a table covering 8 of the 81
classes comfy ships, so Krea 2, Qwen, WAN, LTXV, Z-Image and friends read as
"unknown" and skipped the check entirely - an SD 1.5 LoRA on Krea 2 patched
nothing, changed nothing, and said nothing. It now measures the applied-patch
count across model and CLIP before and after each row and warns when a row
patched nothing, which needs no table, no LoRA metadata, and works on every
model family. base_model_family also stopped mining ss_sd_model_name: that is
the trainer's source filename, and reading substrings out of it labelled any
..._v1.safetensors as SD1.5 and anything containing xl as SDXL.
LM Studio Chat: reasoning returned in the server's own reasoning_content field
now reaches the thinking output. Only inline <think> tags were read, so with
gemma/qwen-style hybrids the whole reply landed in a field the node never
looked at and text came back blank. An empty answer now explains itself,
naming the max_tokens budget when the model ran out of room mid-thought.
Compare: the stage follows the node's height, not only its width. Its panel
declared a computeSize, and the widget layout gives any widget that defines one
a fixed height and leaves it out of the leftover-space split, so dragging the
node taller only added dead space under the image.
Pad Image / Load Image + Pad: a stitcher output, so an outpaint can put the
source back exactly. Padding now builds the same stitcher shape Crop For
Inpaint emits - the crop is simply the whole canvas - so the existing Stitch
Inpaint node serves both and no second stitch node has to exist. Appended
last, so saved workflows keep their links.
Settings menus: editing one row no longer reverts the others. Each save handed
back a value set rebuilt from stored defaults, silently replacing every other
row on a node whose workflow differed from those defaults.
LoRA templates popover: Save was clipped off the panel edge and each saved
row's delete x was stretched across the whole row - one blanket
.ausboss-lora-menu button rule outranked the specialized buttons inside it.
In hold mode the stage captured the pointer on press, which retargeted the
release and ate the mode button's click - one way into HOLD, no way out.
SLIDE and HOLD are now two labeled buttons, and presses born on the
toolbar never reach the stage behaviors.
The frontend sizes a DOM widget's wrapper as widget.width ?? node.width
(GraphView, useDomWidget updateWidgets), and LiteGraph's layout plants
widget.width during draws. Once planted it outranks the node width
forever: resize the node narrower and the wrapper keeps the old width,
parking the panel's right edge - and the LoRA row's strength and info
controls - outside the border. None of the containment guards could
reach it: border-box and the overflow clip act at the wrapper's edge,
which is precisely the thing that is too wide, and the minimum-width
paths only stop the node going BELOW a floor, not the wrapper staying
ABOVE the node.
Diagnosed from a console measurement in the reporting browser (wrapper
styled 470px on a 328px node, scale 1.21) and reproduced exactly by
planting widget.width = 470: overflow +183px. keepDomWidgetWidthAuto
defines width as a discard-writes accessor, so the frontend's lookup
stays on its node-width fallback for the life of the widget. All six
DOM panels call it, the policy test requires it, and the fix was
verified end-to-end in a live classic-renderer session: with the plant
attempted, the wrapper tracked the node through narrow, add-row,
linked, grow and shrink at -13px inside the border throughout.
An OUTPUT_NODE that saves temp previews of each batch's first frame
and renders them stacked in a DOM panel: slide mode clips B to the
pointer with a seam line, hold mode flashes B while pressed. image_a
passes through unchanged so the comparison can sit mid-graph. Pure
clip and mode logic lives in js/shared/compare.mjs under node:test.