Skip to content

Release v0.16.0-alpha - #90

Merged
MehranMarxian merged 15 commits into
mainfrom
release/v0.16.0
Aug 22, 2026
Merged

Release v0.16.0-alpha#90
MehranMarxian merged 15 commits into
mainfrom
release/v0.16.0

Conversation

@MehranMarxian

Copy link
Copy Markdown
Owner

Release PR for v0.16.0-alpha. A UI pass plus one new generation capability, on top of the theme-token refactor.

What's in it

  • Artist-Friendly Dark, a third theme, built on the extracted colour tokens so it is ~40 token overrides rather than a fork of the panel. Compact Adobe Dark is byte-identical.
  • Slider faces for the numeric parameters in Artist-Friendly Dark — one control, two faces; the number input stays the source of truth. Compact keeps its number boxes.
  • A dice button on all five seed fields, and the fix underneath it: a UXP <input type="number"> clamps at 214748.36, so every wide seed — including every seed loaded from History — came back mangled and failed the run with "Seed must be a whole number". Seed fields are now numeric text inputs.
  • Advanced settings disclosure remembers its per-screen open/closed state, stored apart from generation defaults so Reset Settings doesn't re-collapse it.
  • inpaint-flux2-klein — masked inpaint on the Klein 4B stack already installed for Text to Image, no new model download; needs the same comfyui-inpaint-cropandstitch pack as the crop-and-stitch Flux Fill preset.
  • Version bumped to 0.16.0 across all eight locations; CHANGELOG, README and both landing pages updated.

Validation

npm run typecheck, npm test (868 passing incl. version consistency), npm run build all green.

Smoke checklist (real Photoshop + ComfyUI on :8190)

  1. Footer reads v0.16.0 in the panel.
  2. Settings → switch to Artist-Friendly Dark: panel restyles; switch back to Compact Adobe Dark and confirm it looks unchanged.
  3. In Artist-Friendly Dark, Detail/Guidance/Strength are sliders; drag one, confirm the number box tracks it and the generation uses that value. In Compact they're still number boxes.
  4. Dice button: each press rolls a different seed; the field never shows 214748.36. Then load a History entry and generate from it — its seed loads and runs (this failed on every History load before).
  5. Advanced settings: expand on one tool, collapse on another, reopen the panel — each screen remembers. Reset Settings must not re-collapse a screen you left open.
  6. Inpaint → select → inpaint-flux2-klein → generate: repaint blends; strongest at adding to a small selection and replacing a large region. Editable Steps/CFG/Denoise (unlike Flux Fill, which locks them).
  7. Confirm ordinary generation on the other tools is unaffected.

Klein inpaint was already smoke-tested and confirmed by Mehran before the paperwork commit.

MehranMarxian and others added 15 commits August 19, 2026 22:20
The v0.16 artist-friendly control work rests on two things this panel has
never shipped: a range input, and an icon that is not a bitmap img. UXP has
already refused visibility:hidden, flex gap in compact panels, and sticky
reflow, so a browser check proves nothing. This renders the candidates in
Settings so one Photoshop screenshot answers all four questions at once.

Delete as a unit with src/ui/spikeArtistControls.ts once the design lands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Round 1 settled the premise and killed two assumptions. type="range" renders,
drags and fires change events, so the slider plan holds. But UXP ignores step
-- sliders with the HTML default of 1 returned 40.541, 47.147 and 45.675 --
and appearance:none with ::-webkit-slider-thumb did not take, leaving the bare
native slider looking better than the styled one. A data-URI SVG background
rendered nothing at all, while inline SVG, unicode and CSS shapes all worked.

Round 2 therefore stops replacing the widget and asks whether accent-color can
brand it, whether an explicit step quantises, and whether the label-as-prefix
row survives a narrow panel.

snapToStep is carried by tests rather than the spike, because quantisation is
now mandatory in the real controls: without it a steps field submits 20.4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.16 wants a second theme, but there is effectively only one theme to
build it on: the stylesheet measured 9 unprefixed base rules against
1,158 .theme-compact rules, with 559 hardcoded hex values against 24
colour tokens. Adding a third theme by hand would mean duplicating those
1,158 rules, which is exactly how Classic v0.4 died.

Define 41 semantic tokens on :root and route the stylesheet through them
so a theme becomes ~39 token overrides. Every replacement is either an
exact match for the value already there, or a near-match within 1-3 units
per channel, so this is intended to be visually inert. New names cover the
gaps the old set had no word for: text-soft, text-secondary, link,
surface-alt, surface-strong, hover, hover-border, accent-border,
accent-bright, accent-glow.

Coverage goes from near-zero to 50% (328 var() references against 331
remaining literals). The remainder is mostly one-off decorative colour --
per-workflow category swatches and legacy near-greys -- which is a
deliberate stopping point: those do not belong to the theme and tokenising
them would invent vocabulary we have no use for yet.

Token definitions are skipped by the pass, as are hex values inside var()
fallbacks, so var(--ol-text, #e6e6e6) keeps its fallback intact.

docs/css-audit.md is regenerated by npm run audit-css.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Photoshop results from round 2, all measured on the real panel:

  E. An explicit step DOES quantise -- step="1" returned 44, step="0.05"
     returned 0.55. Round 1's continuous floats (40.541, 47.147) came from
     relying on the IMPLICIT default step, which UXP does not apply. The
     round 1 note claiming "UXP ignores step" was too broad; corrected.
  F. accent-color does NOT take. Combined with round 1 killing
     appearance:none + ::-webkit-slider-thumb, there is no cheap recolour
     of the native slider at all. Branding has to live in the label row.
  G. JS snapping produces clean values. Keeper.
  H. The Krita AI label-as-prefix row reads well in a narrow panel. Keeper.

Round 3 asks the one untried route: does background on the range ELEMENT
paint the track? UXP's native default is #535353, which floats a light bar
on the #2b2b2b that .panel-section resolves to in the compact theme --
Mehran flagged the mismatch on sight. The rule uses var(--ol-panel) rather
than a literal so the match survives a theme change.

The three probe rows are rebuilt as the controls we actually intend to
ship -- Detail (steps), Strength (denoise), Guidance (CFG) -- each with a
declared step, snapToStep on top, and the dual naming from the research
pass. Testing the real shape is worth more than testing another abstract
widget, and it puts the "Off" special-case in front of a real reader.

snapToStep stays regardless of E: preset and persisted-preference values
are not bound by any control's step and still need quantising before they
are shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third theme, offered in Settings alongside Compact Adobe Dark and Classic
v0.4. Compact is deliberately untouched: the only styles.css changes are
the deleted spike block and the appended theme block, so every existing
compact rule is byte-identical.

The load-bearing decision is in applyTheme. Artist-Friendly Dark KEEPS the
theme-compact class and stacks theme-artist on top rather than standing on
its own. The compact rules are the stylesheet -- 1,158 of them against 9
unprefixed base rules -- so a theme with its own class inherits almost
nothing and has to re-state the entire panel. That is how Classic v0.4
ended up a veneer over two elements, and it is what the colour token pass
was for: the theme is now 40 token overrides and nothing else.

The class is mirrored onto body because html/body/#root paint the shell
behind the panel from --ol-section-bg and are ancestors of the app shell,
so a token set only on the shell cannot reach them.

Palette intent: warm the neutrals off pure grey, drop the chrome contrast
so the artwork is the brightest thing on screen, and soften #ffffff text to
a warm off-white. The outer shell moves from #535353 to #3a3634 -- compact
currently frames the panel in something brighter than the image inside it.

Theme handling had no test coverage at all. normalizeTheme replaces two
hand-written ternaries that silently mapped anything unknown to compact,
and the class stacking is asserted directly, because that failure is
invisible: the panel still renders, just missing rules nobody notices
until a screenshot.

The spike is deleted now that all its questions are answered (round 3
confirmed background on the range element does not paint the track either,
so there is no CSS route to the native slider's colours). snapToStep is
promoted to src/utils with its tests -- explicit step covers the widget,
but preset and persisted values are bound by nothing and still need
quantising before display.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Steps, CFG, denoise, sketch influence and live-paint strength become
sliders in Artist-Friendly Dark. Compact Adobe Dark still shows the same
number boxes it always has.

One control, two faces. The number input stays the single source of truth
and stays in the DOM in every theme; the slider is injected beside it,
writes through to it, and re-dispatches input and change so every handler
bound before it existed fires exactly as before. Nothing is rebound and no
view code is refactored -- CSS alone decides which face is painted. The
alternative, building a separate artist control, would mean two
implementations of every parameter and guaranteed drift, which is the one
thing the earlier "sliders in both themes" decision was there to prevent.

Labels lead with the artist word and keep the jargon searchable, per the
naming decision: "Detail (steps)", "Guidance (CFG)", "Strength (denoise)".
Denoise reads as a percentage, because 60% is a quantity an artist already
has a model for and 0.6 is not.

Slider ranges are deliberately narrower than the typed input allows --
steps stop at 60 where the field accepts 150, CFG at 12 where it accepts
30 -- because a slider that spends most of its travel in territory nobody
uses is a worse control than a short one. widenToFit extends the range
rather than clamping when a real value lands outside it, so switching
theme can never silently rewrite a setting. That is SwarmUI's ViewMax.

Every range declares step explicitly: UXP does not apply the implicit
default and returns continuous floats without it. snapToStep runs on top
because preset recommendations and restored preferences are plain numbers
bound by no control.

Adds jsdom, scoped to the new test file by docblock rather than switched
on for the suite. The existing DOM-touching tests stub their two calls to
keep jsdom out of a pure-logic suite, and that stays true; but here the
DOM contract IS the thing worth testing, and a hand-rolled fake would end
up testing the fake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…witch

Three faults from the Photoshop test, two of them mine to have caught.

CRASH (steps 90 in Compact, then switch to Artist). Nothing in the app
listens to the steps/cfg/denoise inputs -- generation reads .value directly at
submit time -- so the input/change events being re-dispatched had no real
consumer, while the module's own sync listener WAS subscribed to them. UXP
is not a browser DOM and may echo a programmatic .value assignment back as
an `input` event; unguarded, that is unbounded recursion, and a stack
overflow inside UXP takes Photoshop with it. A re-entrancy latch now makes
re-entry impossible by construction, and `change` fires once when a drag
ends rather than on every pixel of travel. There is a test that simulates
the echo and requires it to settle.

LEAK INTO COMPACT. The compact sheet carries
    ... .settings-grid > .field > input { display: block !important; width: 96px !important }
An injected <input type="range"> IS an input, so it matched at (0,5,1) with
!important and beat a plain `display: none` -- [hidden] would have lost too.
Fixed twice over: the slider now sits inside an .artist-row wrapper, and the
compact rules use child combinators so a grandchild never matches; and the
row is only built while the artist theme is active, torn out entirely on the
way back, so Compact's DOM is byte-identical to before this feature existed.
Removing beats hiding here, which is the same lesson as the nav separator.

OVERSIZED NUMBER BOXES. Same root cause -- the injected elements were extra
children inside a `display: grid` field, so the grid grew tracks for them.
With the compact face hidden and the row nested, the field lays out as one
block again.

Hiding the compact face still has to outrank rules at (0,5,0), (0,5,1) and
(0,6,0), all !important. .field is sometimes a <div> and sometimes a
<label> so it cannot be qualified by element, and UXP will not let CSSOM
inline styles beat !important either. Selector weight is the only lever
left, so .has-artist-slider is repeated three times to clear all of them
outright. At two repeats the field rule merely ties and the label rule
loses, which is precisely the reported bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r reach

Sketch to Image was showing all three faces at once -- truncated compact
label, slider row, and number box side by side -- while Text to Image was
correct. The override rules were losing to a per-view restatement of
`.field > .label` that outweighed them. Winning that on specificity means
keeping a repeated-class hack correct against 1,158 compact rules restated
per view, and the last attempt at it was already wrong in one view out of
several.

So the field's own label and number input are now MOVED INTO .artist-row
when the theme builds, and put back in their original order on teardown.
As grandchildren of the field they match no `.field > X` rule at all, in
any view, so there is no fight left to lose and the CSS drops back to
plain selectors. Not being in the fight is more robust than winning it.

The double-build guard had to move with them: once built, the input's
parent IS the row, so checking only for a descendant row looked inside it
and nested a second one. Caught by the existing "enabled twice" test.

Also attempts the slider track colour via ::-webkit-slider-runnable-track.
UNVERIFIED -- accent-color, background on the element, and
::-webkit-slider-thumb are all already disproved in Photoshop, but the
track pseudo-element itself has never been tried; round 1 only ever tested
the thumb. If it does nothing then the native track cannot be themed and
that is the honest answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PROGRESS BAR. The determinate fill's actual root cause: a percent label was
painted via `.status-progress.is-determinate::after { content: attr(...);
float: right; margin-top: -14px }` -- text lifted above a 3px bar with
`overflow: hidden` on its own container. That combination is fragile even
in a browser; UXP does not render it as intended, and the clipped glyph
pixels are what showed as the strange jagged shape. Deleted; the same
percent already prints in the ordinary status text line above the bar
("Generating step 18 of 20 (90%)..."), so nothing is lost.

Restyled while in there: height 3px to 5px, fill uses --ol-accent instead
of an unrelated blue, with a soft glow, so the bar reads as the same
"working" colour as the amber status pill instead of a mismatched hue. Also
removed the CSS keyframe sweep animation and its enabling rule -- dead code,
permanently overridden by a later `animation: none !important` in favour of
the JS-driven marginLeft interval that actually animates the indeterminate
state. And dropped the `data-progress-label` attribute writes in
statusBars.ts now that nothing consumes them.

SEED DICE BUTTON. Artist-Friendly Dark only, same reasoning as everywhere
else in this theme: nobody should have to type a 64-bit integer as the
primary way to pick a seed. Click rolls a fresh one into the field. The
number input stays, unlike the sliders -- reproducing a seed you already
know (from History, from a note) is still typing a number in, and that path
is worth keeping next to the new one rather than replacing it.

Same lifecycle and the same reason as the slider face: built only while the
theme is active, torn down on the way back, input re-parented into a row so
it escapes the compact sheet's `.field > input` rules rather than fighting
them for specificity. No re-entrancy latch is needed here, unlike the
sliders -- there is no second widget mirroring the value, and nothing
listens for input/change on a seed field, so there is no listener for a
UXP-echoed event to call back into.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mehran asked for a Simple/Advanced mode. The panel already had the core of
it -- every settings grid (steps/CFG/seed and friends) sits behind a
per-screen "Advanced settings" disclosure, defaulting to collapsed since
v0.6 #8 -- it just forgot the choice on every reopen. Given the choice
between that and a new global mode switch that would replace five
independent per-screen disclosures with one, he picked polishing what
already works: smaller, safer, and does not force every screen to share one
setting when a user might reasonably want Text to Image expanded and
Outpaint collapsed.

Persisted under its own storage key, following the same pattern as the
preview panel pin and Agent Bridge settings: not part of the Text to Image
form's OpenLayerPreferences object, because Reset Settings resets generation
defaults and should not silently re-collapse a screen the user chose to
leave open. Keyed by each grid's own aria-label ("Generation settings",
"Outpaint settings", ...), which was already unique per screen and needed no
new DOM attribute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uttons

SEED DICE, two faults in one control.

The die never painted: an inline <svg> assigned through innerHTML rendered
as an empty box in Photoshop. Rebuilt from three <span> pips positioned on
the diagonal, with the button's own border as the die outline -- CSS shapes
are the one icon technique the spike confirmed works, and these are created
as elements rather than parsed from markup, so there is nothing to fail to
parse. Emoji and geometric glyphs were never an option here; the advanced
disclosure already uses a plain-text caret for the same reason.

Every roll produced the same 214748.36 because a UXP <input type="number">
cannot hold a value above the signed 32-bit maximum, and everything larger
comes back out as 2147483647 with a decimal point pushed into it. rollSeed
was drawing from the full MAX_SAFE_INTEGER range, so every roll overflowed
to the identical mangled value and the button looked inert. Capped to
2^31-1. Note the display bug predates the dice -- 214748.36 is visible in
the seed field in screenshots from before it existed -- the button only
made it reproducible on demand. Generation is unaffected: readSeed in
comfy/settings.ts still rolls its own full-width seed when the field is
blank.

PROGRESS BAR. Removing the clipped pseudo-element label was necessary but
not sufficient; it was still jagged. Three things were asking UXP to
rasterise curves and blur on a box five pixels tall: a 999px radius (the
degenerate case -- the same radius is fine on the status pills, which are
tall enough to have a real curve), a blurred box-shadow glow, and a
gradient fill. All three are gone. A flat block with a 3px radius has no
curve to approximate and no blur to sample.

PROMPT BUTTONS. Flush against each other at different heights, for two
unrelated reasons. They are not siblings: #suggest-prompt is a direct child
of the generator panel while #negative-prompt-toggle is wrapped in
.negative-prompt-section, both wrappers zero their children's margins with
`> * { margin: 0 !important }`, and the section then spaces its children
with flex `gap`, which is inert in compact UXP panels. Separately, the
toggle carries .disclosure-button, which sets its own min-height. Fixed by
ID, which outranks every class rule involved. Scoped to the artist theme so
Compact Adobe Dark keeps the layout it has always had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ROOT CAUSE, finally. A UXP <input type="number"> clamps at roughly
214748.36 -- that is 2147483647 scaled by 1/10000 -- so any seed wider than
six digits came back out as that identical string. Two rounds of capping
the roll chased that ceiling without reaching it, because the ceiling is
four orders of magnitude lower than the signed 32-bit max it looks like.
The input type was the cause, not the number being rolled.

This was never only cosmetic, and it was never only about the new button.
readRequiredInteger in comfy/settings.ts rejects non-integers, so a mangled
seed fails the run outright with "Seed must be a whole number" -- and
loading any History entry was enough to trigger it, since those seeds are
full-width server-side rolls. 214748.36 is visible in the seed field in
screenshots from before the roll button existed.

All five seed fields become type="text" inputmode="numeric". Nothing else
had to change: the value is read as a string and parsed by readSeed either
way. The roll now uses the same MAX_SAFE_INTEGER range createRandomSeed
already uses server-side, so there is one concept of "a seed" again.

This touches Compact Adobe Dark, deliberately and for the first time. It is
a correctness fix for a bug Compact has had all along, not a restyle -- the
field looks identical, it just stops corrupting values. Flagging it rather
than burying it, given the standing instruction to leave Compact alone.

BUTTON LABEL. The die is gone; the button says "Roll". Two icon techniques
were tried in Photoshop and both painted an empty box -- an inline <svg>
assigned through innerHTML, then three CSS-shape pips on the diagonal. With
data-URI SVG and emoji already ruled out by earlier work, text is the only
technique left that has never failed to render in this host. It is also the
better control for this theme's audience: "Roll" says what it does, where a
die face has to be interpreted.

Adds a markup-level regression test. The bug lives in an attribute, and
nothing else in the suite would notice type="number" coming back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Klein and Krea2-Turbo both shipped for Text to Image and Image to Image with
no inpaint path, and research had pointed at scraed/LanPaint as the technique
that would give them one. The spike that preceded this commit installed
LanPaint on the shared ComfyUI instance and compared it head to head against
the cheapest alternative -- SetLatentNoiseMask with the ordinary KSampler --
and found the interesting axis was neither of them.

What decides Klein inpaint quality is how much of the sampled frame the mask
occupies. Sampling the whole captured context, a small mask asked to *add*
something produced the surroundings again and ignored the prompt in five of
six runs across both models: clean, well-blended, tone-matched, and not what
the artist asked for. Cropping to the mask plus 50% and sampling that at 1024
fixed it in three of three. That is why InpaintCropImproved and
InpaintStitchImproved are required nodes here rather than the refinement they
are for inpaint-flux-fill-cropstitch, and why context_from_mask_extend_factor
is load-bearing -- at 1.5 the prompt lands, at 3.0 it stops landing.

So LanPaint is not adopted. It does fix the same failure, but crop-and-stitch
fixes it with a node pack the registry already declares, and stacking both was
worse than either: LanPaint_ImageDecode and InpaintStitchImproved each
composite and feather the patch, and the doubled blend showed at the seam.
No new dependency, no setupManifest change, and no new model download -- this
preset loads the same Klein 4B stack Text to Image already needs.

The one piece of shared machinery that had to change: the alpha-embedded mask
upload was gated on isFluxFillPreset, which also selects the locked Flux Fill
sampler defaults. Those two properties come apart here, so
presetUsesEmbeddedMaskAlpha now names the upload behaviour separately. Reusing
the old predicate would have written Flux Fill's 20 steps at guidance 30 into
node "3", which exists in both graphs, so nothing would have thrown.

Krea2-Turbo was built on the same graph and deliberately not shipped: it
follows prompts but will not tone-match a small mask, and its two fixes
conflict. docs/inpainting.md records the measurements and what reviving it
would take.

Smoke-tested in Photoshop by Mehran: small additive selection, large garment
replacement, context-sized output, aligned import, editable sampler controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bump the version to 0.16.0 across every location the consistency test names
-- package.json and its lock, the bridge package and its lock, the UXP
manifest, APP_VERSION, and both landing pages -- and write the release notes.

This release is a UI pass plus one new generation capability. Artist-Friendly
Dark is a third theme, built on the colour tokens the CSS was refactored into,
and it is the theme that turns the numeric parameters into sliders while
Compact Adobe Dark keeps its number boxes. The seed field gains a dice button,
and underneath it the fix for a real bug: a UXP number input clamps at
214748.36, so every wide seed -- including every seed loaded from History --
came back mangled and failed the run. The Advanced settings disclosure now
remembers its per-screen state. And inpaint-flux2-klein gives the fast Klein
4B stack a masked-inpaint path with no extra model download.

CHANGELOG, README (checkpoint line, New-in block, tester focus, boundaries,
package filenames, quick pass) and both landing pages updated. Prose claims
checked against git log v0.15.0..HEAD and the code, not the task list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This release adds the project's first jsdom-environment tests -- seedDice,
artistControls and advancedToggle, covering the DOM contracts of the slider
faces, the dice button and the Advanced-settings persistence that the pure
functions cannot reach. jsdom 30 pulls undici 8, whose worker calls
webidl.util.markAsUncloneable; Node 20's bundled webidl does not have it, so
vitest's forks pool failed to start those three workers in CI while every
test passed locally on Node 24. The suite ran 824 tests green and errored
only on the three worker starts.

Node 22 is current LTS and carries the undici that jsdom 30 expects. The
build (vite) and the bridge run on it unchanged; this only moves CI's runtime
forward, no source or dependency change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MehranMarxian
MehranMarxian merged commit 3bfaceb into main Aug 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant