Commit Graph
68 Commits
Author SHA1 Message Date
DumiFlex 1c305e4fb1 Merge remote-tracking branch 'origin/main' into feat/tag-axes 2026-08-12 03:09:26 +03:00
DumiFlex 72e96ec73f fix(release): ship locales/ in the release zip, and make the ship list total
`SHIP_PATHS` omitted `locales/`, which ComfyUI reads from
`custom_nodes/<pack>/locales/<lang>/` for node display names,
descriptions and settings labels (app/custom_node_manager.py). We ship
main.json, nodeDefs.json and settings.json there.

The registry archive already included it, because `comfy node publish`
ships everything git-tracked that `.comfyignore` does not exclude. The
zip is a hand-maintained list, so the two artifacts silently disagreed
about what a complete install is. Nothing installs from the zip today,
which is the only reason this has not bitten anyone.

The omission is the symptom; two hand-maintained lists of the same thing
with nothing tying them together is the cause. `assertShipListIsTotal`
now fails the pack when a top-level tracked path is neither in
SHIP_PATHS nor explicitly listed as dev-only, so the next runtime
directory someone adds cannot go missing the same way.

Also names the missing-`zip` case: on Windows the packer reported
"exited with status null", which says nothing about `zip` not being on
PATH.
2026-08-09 01:52:46 +03:00
DumiFlex 33c2e718c1 ci: build the booru tag list on demand, publish to a fixed URL
Data source for the optional value autocomplete (#27). Replaces the
community script this started from, which walked all 1000 allowed pages and
kept only `name,post_count`.

Three changes, each from something measured against the live API on
2026-08-06.

A post-count floor instead of a page count. `search[order]=count` sorts
strictly descending ACROSS pages:

    page    1: first 8,244,950   last 55,918
    page  100: first        68   last     67
    page  800: first         1   last      1

so a popular tag cannot hide on a later page and stopping early provably
loses nothing. Walking to page 1000 spends ~900 requests and a quarter of an
hour collecting single-post tags. The default floor of 20 lands near what
other extensions ship.

Aliases, from the separate `tag_aliases.json` endpoint. `tags.json` carries
none, and aliases are what let someone type `hires` and land on `highres` --
demonstrably the most valuable field after the names.

Category is kept. The API returns it and the old script discarded it; it is
what lets the UI tell an artist tag from a character tag.

Also retries with backoff rather than aborting on the first non-200, which
over a 100-request run means one blip no longer discards everything.

The User-Agent is pinned to bare `product/version` and must stay that way --
Danbooru's bot filter 403s a bare token, a longer description, or anything
containing a URL. Adding a contact URL is the usual courtesy for a scraper and
is exactly what breaks it, so the reasoning is recorded next to the constant.

MANUAL ONLY. The list describes Danbooru, not this project, and goes stale
slowly: a two-year-old file still autocompletes correctly because staleness
moves post counts and therefore ranking, while tag names barely change. That
is not an argument for a timer against someone else's API. Output goes to a
fixed non-semver `tag-list` tag, replaced in place, so the download URL is
stable and the SPA can hardcode it -- and so semantic-release ignores it.
2026-08-06 18:33:02 +03:00
DumiFlex fd9ba60bec perf(packaging): stop leaking every previous build into the install
Investigating slow ComfyUI page loads on a real install turned up this
project as the single largest contributor, by a wide margin.

    js/    518 files   36.0 MB    179 stale chunk copies
    web/  1256 files   22.0 MB   1120 stale chunk copies

ComfyUI fetches EVERY *.js under WEB_DIRECTORY at page load, so that install
requested 237 of our chunks where 58 were live: 9.9 MB, 42% of all extension
bytes on the page and 27% of all extension requests. Twelve copies of `about`,
nine of `ContextWidget`.

Two independent causes, both ours.

Chunks are content-hashed, so every release emits new filenames, and Manager
installs an update by extracting over the installed folder without deleting
what the new version dropped. Each mechanism is reasonable alone. Together
they never remove anything. It stayed invisible for a dozen releases because
`emptyOutDir` wipes the output directory on every local build -- only installs
that UPDATE accumulate, and a maintainer never sees one.

And `sourcemap: "hidden"` still WRITES every .map, omitting only the
`sourceMappingURL` comment, so 22.5 MB of maps shipped that no browser had
ever requested. `pack-release.mjs` excluded them from the GitHub zip, but the
registry package comes from pyproject's `includes = ["js", "web"]`, which has
no exclusion -- so the fix has to be not emitting them.

Now: each build writes `.wp-assets.json` listing what it produced, and the
plugin prunes anything hashed and unlisted at import, before ComfyUI scans
WEB_DIRECTORY. Self-healing -- existing installs clean themselves on the next
start with no user action.

Deliberately conservative, because deleting a live file breaks an install
while missing a stale one only costs disk: no manifest, an empty manifest or a
corrupt one all mean do nothing; only bundler-shaped names are candidates
(hashed chunks, plus any .map -- the entry map is unhashed, which the hash
pattern alone walked straight past); every failure is swallowed.

Verified against a copy of the real install, updated the way Manager would:
1,881 files -> 302, 60 MB -> 10.9 MB, 1,579 removed. All 551 relative module
imports across the surviving chunks still resolve, and every asset
`web/index.html` references is intact.
2026-08-06 17:58:01 +03:00
DumiFlex c3e74e206d perf(extension): stop bundling the webfonts into JavaScript
Vite library mode ignores `build.assetsInlineLimit` and inlines every asset
unconditionally, so both woff2 files were base64'd into a chunk: 88,660 bytes
of font became a 118,896-byte JS file, awaited in main.ts's top-level
`Promise.all` and therefore parsed as JavaScript before ComfyUI could finish
registering the nodes.

fonts.ts believed the opposite -- its comment claimed `new URL(path,
import.meta.url)` kept the woff2 as separate assets. That transform is exactly
what Vite intercepts in lib mode, so the intent never held.

The fonts now leave the module graph entirely. `src/assets/fonts/wp-fonts.css`
is a real source file copied verbatim to `js/assets/fonts/` by copy-fonts.mjs;
fonts.ts <link>s it at a URL resolved against its own chunk, and the browser
resolves each woff2 relative to the stylesheet. No path arithmetic in JS, and
a renamed install directory still works.

The href is assembled from array parts on purpose: Vite's asset transform fires
on `new URL(<string literal>, import.meta.url)`, so a literal here would
silently re-inline the stylesheet with no build error and no failing test.

Measured with the gate's own method: 492,407 -> 403,055 bytes gzipped. Total
bytes on a cold cache are about unchanged -- gzip recovers base64's overhead --
so the win is latency and caching, not payload: registration no longer waits,
90 KB left the JS parse path, and the fonts stop being re-downloaded on every
release when the enclosing chunk's hash changes.

TOTAL_LIMIT tightened 490 -> 420 KB. Re-inlining costs ~90 KB, which fits under
490 KB and would have regressed unnoticed; it does not fit under 420 KB.

Verified against a running ComfyUI: the stylesheet and both woff2 serve 200 and
are fetched as fonts, and document.fonts.check() passes for JetBrains Mono,
which only this stylesheet provides.
2026-08-06 02:01:40 +03:00
DumiFlex c7f4d91a10 fix(announce): never emit a half-formed role mention
Unset and empty were already safe. A MALFORMED id was not: any truthy
string went straight into `<@&…>`, so a stray space in the secret produced
a literal `<@&   >` and a pasted mention produced `<@&<@&123>>` — visible
junk at the top of the most public message the project sends.

`normalizeRoleId` is now the single decision point, applied inside
`buildPayload` rather than at the call site so every caller gets the same
guarantee. It trims, unwraps a pasted `<@&123>`, and requires a 17-20
digit snowflake. Anything else is rejected rather than guessed at: a ping
that silently does not happen is a far smaller problem than a broken one
that does.

`allowed_mentions.roles` is derived from the same normalised value, so
the two can never disagree — a content string naming a role the payload
does not permit would be a mention that renders but never notifies.

The CLI warns when a non-empty id was supplied and rejected. Dropping it
silently would leave someone wondering why the ping never fired.
2026-08-04 20:05:45 +03:00
DumiFlex 45a9092bbc feat(announce): two channels — staff automatically, public by hand
One webhook meant the choice was all-or-nothing: either every release
announced itself to everyone, or nothing was automated. Two channels,
deliberately asymmetric:

  staff   posts AUTOMATICALLY on every release
  public  ONLY from a deliberate manual dispatch

The staff copy carries a link back to this workflow with the inputs
spelled out. That is what makes it a review step rather than a duplicate
feed: the post being read IS the post that will go out, and the button to
send it is attached to it. Otherwise "now go publish it" is a separate
instruction living in someone's head.

The public copy does not get that field — an announcement should not tell
its readers how to re-send it. Returned as `undefined` rather than an
empty array, since an empty field block renders as dead space.

Guarded twice. The workflow only passes `--channel public` on a manual
dispatch, AND the script refuses public when `GITHUB_EVENT_NAME=release`.
One of those is enough; both are cheap, and the failure mode is
announcing to everyone with nobody having read it.

Only the public post pings the role. A ping in the staff channel on every
release is noise in a room that exists to watch releases.

`DISCORD_WEBHOOK_STAFF` / `DISCORD_WEBHOOK_PUBLIC`, with the existing
`DISCORD_WEBHOOK_URL` kept as a staff fallback so the current
single-webhook setup keeps working untouched.
2026-08-04 20:01:25 +03:00
DumiFlex 2dfed26c1e ci(announce): post as Wildcard Pipeline, not as the webhook's own name
v2.12.0 went out as "Captain Hook" with a placeholder avatar — a webhook
posts under whatever name and image it was created with, and Discord's
default is a generic pair.

`username` and `avatar_url` override that per message, so the identity
lives in the payload rather than in a webhook's settings. It then stays
right if the webhook is ever rotated or recreated, and it is the same in
every channel the automation posts to.

Avatar is the 512px manifest icon, fetched from raw.githubusercontent by
Discord server-side. It has to be raster — an SVG will not render, which
rules out the repo's favicon.svg, and that failure mode is silent. Pinned
to `main` rather than a tag so replacing the logo needs no code change.

`DISCORD_USERNAME` / `DISCORD_AVATAR_URL` override both. An empty value
falls through to the defaults rather than blanking the identity and
handing the post back to the webhook's name.

The dry run now prints both, since identity is invisible in the embed
body and is exactly what was wrong the first time.
2026-08-04 19:52:51 +03:00
DumiFlex f240aeaf05 docs(release): bring the notes up to date, and declare Nodes 2.0 in the README
Ten commits landed after `next.md` was written and none of them were in it.
Anything left out of that file simply does not ship in the notes.

No new highlights. The cap is 4-5 and it was already at 5 — per the file's
own rule, if a sixth feels essential something else was not a highlight.
Per-frame lock and bypass is a shortcut to something the seeds dialog
already did, so it goes in the tail rather than displacing anything.

Tail gains six entries in two new groups: the frame chips and collapsible
grid on the Loop and Seed List, the broken-reference fix, the what's-new
page, and the update check that was burning GitHub's hourly allowance.
Count corrected 20 → 26, which the update dialog computes independently
from the same bullets.

Also fixes a real bug in the announcement transform, caught by previewing
it against this file: `splitBody` stripped `<details>` BEFORE counting the
tail. The "Also in this release" bullets live inside that block, so it
counted nothing real — while the authoring guidance in the trailing HTML
comment contributed bullets of its own. A 26-change release announced
itself as 3. Comments are stripped, details are not.

README gets the Nodes 2.0 line. Renderer support is a compatibility fact
people check before installing, it was the single largest piece of work in
this release, and the README did not mention it anywhere. The "switching
between them with a workflow already open" clause is deliberate: that is
what repeatedly broke and is now specifically handled.
2026-08-04 19:13:59 +03:00
DumiFlex 911e9839a4 feat(release): derive the Discord announcement from the release notes
Announcements were written by hand, which is why they went out "only once
a few versions" -- the cost was never posting, it was composing a second
piece of prose about a release that had already been written up once.

Nothing new has to be written. `docs/release-notes/next.md` is already
curated by hand per release and already IS the GitHub release body, so
the announcement is a transform of it rather than a fresh document.

`scripts/lib/discord-notes.mjs` handles the gaps between GitHub markdown
and Discord, which are specific rather than general:

  - masked links render ONLY inside an embed, so the notes go in an embed
    and the message content stays a bare ping
  - there are no tables; rows become bullets
  - <details> shows as raw tags
  - headings render, but an embed `##` outsizes the embed title above it,
    so headings become bold
  - the description caps at 4096, and truncation cuts at a bullet
    boundary -- a hard slice can land inside a link and render as broken
    markup, which is worse than saying less

It cuts at the same `<!-- /modal -->` marker the in-app update dialog
uses, for the same reason: an announcement is an interruption. The tail
is counted rather than dropped, so a three-bullet post over a forty-change
release does not read as a quiet week.

`allowed_mentions` is always explicit. Release notes are not written with
`@everyone` in mind and a webhook will happily resolve one that slipped
in; only a configured role id can ping.

Two entry points. `release: published` posts automatically. A manual
`workflow_dispatch` takes `--since <tag>` and rolls several releases into
one post -- the catch-up case that actually happens -- and defaults to a
dry run so a manual trigger cannot surprise anyone. Dry run is also the
CLI default: `pnpm announce --version v2.11.0` prints and posts nothing.

Kept as a separate workflow from release.yml on purpose: a Discord
outage, a rotated webhook or a rate limit must never fail a release that
has already shipped.

Also widens the vitest include to `scripts/**/*.test.mjs`. A broken
announcement transform is otherwise only noticed after it has been
posted.
2026-08-04 19:06:03 +03:00
DumiFlex 51286b3273 fix(docs): spread the figure wash across the panel, not the corner
The CSS this mirrors is `radial-gradient(120% 100% at 0% 0%, …)` — an
ellipse 120 percent of the WIDTH by 100 percent of the HEIGHT, anchored
top-left, so it falls off across the whole panel. I had applied a
`scale(1, h/w)` transform, which squashed it into a flat smear along the
top edge; on an 880x202 frame that reads as a tinted corner with three
quarters of the figure left flat.

Now `r=1` with `scale(1.2 1)`, which is the CSS geometry directly, and a
three-stop falloff so the tint carries further before it disappears.
2026-08-04 07:23:11 +03:00
DumiFlex 157606b082 fix(docs): restore the figures' depth, and draw the loop_config link
Three things, all of them fidelity to the SPA figure rather than taste.

The frame lost its wash. PipelineDiagram paints
`radial-gradient(120% 100% at 0% 0%, node 6%, transparent 60%)` over
bg-1; dropping it left a flat slab. Reproduced as a radial gradient with
themed stops.

The connectors lost their fade. `.wp-doc-diagram__arrow-ln` is a
gradient that fades in and out at both ends, so a connector reads as
flow rather than a drawn border -- which is most of why these felt dead.
They are RECTS, not lines: a horizontal <line> has a zero-height
bounding box, so an objectBoundingBox gradient on it collapses and the
line vanishes, leaving an arrowhead pointing at nothing. Caught that in
the render, not in review.

And the loop diagram was topologically incomplete. WP_ContextLoop has a
`loop_config` side-output; WP_SeedList takes it as an optional input and
mirrors the loop's count, strategy and base seed from it. That link is
exactly WHY the two fan out in lockstep, and the old figure -- and my
first rebuild -- drew them as unrelated sources, leaving the caption's
claim unexplained.
2026-08-04 07:21:28 +03:00
DumiFlex 2f40c921eb fix(docs): colour the flow-figure icons instead of leaving them black
The icon extractor rewrote `fill` attributes to `currentColor`. Almost
no PrimeIcon HAS a fill attribute -- of the ones these figures use, only
`sparkles` declares one -- so a rewrite-what-exists pass left every
other glyph at SVG's default black.

That is why the feeder pills looked right while every node icon came out
black: the feeders happened to use icons carrying fills.

Concrete fills are now stripped and the wrapping group carries
`fill="currentColor"`, so anything without its own fill inherits the
group colour. `fill="none"` is preserved -- on the icons that use it, it
marks the deliberately hollow part, and filling it would blob the glyph.

Verified rendered in both themes: WP nodes teal, external nodes muted,
feeder pills in their per-kind colours.
2026-08-04 07:17:28 +03:00
DumiFlex 4b69068ace docs: rebuild the README flow figures from the SPA's design system
The two figures were hand-drawn SVG that had drifted from the app: flat
horizontal cards, approximated icon shapes, a violet accent the product
does not use, and — in the loop diagram — the assembled prompt running
straight into KSampler. It does not go there. Text is encoded first and
only conditioning reaches the sampler; seeds are the one thing that
arrives directly.

Both are now generated from the real component. PrimeIcons ships
`raw-svg/`, the same paths the webfont is built from, so the README
carries the actual glyphs rather than lookalikes, and every value is
lifted from PipelineDiagram.vue and theme.css instead of eyeballed:
teal `--wp-node` accent, `--wp-bg-2` cards at radius 10, vertical
icon/name/mono-subtitle layout, kind-coloured feeder pills, per-chip
accent borders, 9.5px uppercase connector labels.

A `prefers-color-scheme` block mirrors the SPA's light theme, so the
figures follow the reader's GitHub theme rather than sitting as a dark
slab on a light page. `pnpm docs:figures` regenerates them.

The SPA components are untouched — they already theme correctly.

Also: the release-notes tail moves into a <details>. On a release this
size it would otherwise double the page. The summary carries the count
so the reader knows what they are opening; the cost, noted in the
template, is that collapsed text is not reliably reachable by
find-in-page. The modal is unaffected — it already cuts above this.
2026-08-04 07:09:56 +03:00
DumiFlex d221ea479f feat(autocomplete): give the @ and $ popover rows an identity
Both popovers rendered one line of text per hit — a name, and for `@` a
trailing "12 opts · a1b2c3d4". That distinguishes entries whose names already
differ, and nothing at all when they don't, which is precisely the case that
sends someone to the popover in the first place. A library here has two
wildcards called `act_plan`; the old rows were byte-identical.

Rows are now built from what the thing actually is:

- `@` rows carry the wildcard's option count, its axes and its tag count,
  with the uuid kept as the last-resort tiebreaker for entries that match on
  every other fact. A funnel marks rows where Tab opens a filter, shown on
  the active row only — on every row it is a column of noise, and the
  affordance is unreachable for rows you are not on.
- `$` rows name the writer. The canvas walked a graph so it reads "written by
  Style FX · ember-marten" and can honestly say a write "overrides 2"; the
  library has no execution order, so the same count reads "2 others bind
  this" instead. That distinction is resolved in one place, so the popover
  and RefChip's hover card cannot drift into telling different stories about
  the same variable.

The header now reports how many entries MATCHED, and says "· showing 50" when
the row cap hid the rest. It first reported `acRows.length`, which is the
post-cap figure — "50 matches" for a query that hit 89. A silent truncation is
how you conclude the wildcard you are looking for does not exist.

Row models live in `utils/suggestion-rows.ts`: pure data-to-data, out of the
2.8k-line editor that hosts them, and directly assertable.

Bundle budget 468 -> 490 KB. The previous bump left 179 bytes free, so every
change since has had to argue with the gate; this one is sized to also cover
the filter panel that follows.
2026-08-02 06:17:35 +03:00
DumiFlex 4cf497b507 feat(chips): name the module and node that produce a $var
The var-chip hover could only confirm a name existed. With near-identical
modules bound to the same var across several nodes, that is not actionable —
the question is always WHICH one wins.

Adds `collectUpstreamProducers`: the same chain walk and last-write-wins
ordering as `collectUpstreamKinds`, widened to keep identity instead of
discarding it. Covers all three producer sources — WP_Context modules,
WP_ContextInjector rows, and the WP_ContextLoop iteration vars (`$name` +
`$name_total`). Muted / bypassed nodes are skipped, matching runtime.

Node identity follows what each node actually has: WP_Context nodes report
their stable `adjective-noun` codename, the same one their header shows;
the Injector and Loop have no codename scheme, so they use a user-set title
and fall back to a plain kind label.

The card now reads "wildcard · Outfit · in dusk-marten" with the module id,
and adds two states worth surfacing: "overrides N earlier writes" (runtime is
last-write-wins, so it names the winner and says others exist) and "internal —
stripped from the final prompt", which is the most confusing state a var can
be in.

`graphAware` is an explicit prop rather than inferred: a graph-aware host that
finds no writer must say "no upstream producer", while the SPA — which has no
chain to walk — must not make that claim at all.

Raises the bundle budget 461 -> 468 KB. Approved standing: the maintainer
asked for the budget to move rather than features to be trimmed to fit.
2026-07-30 16:40:25 +03:00
DumiFlex 3f45b26396 chore(size): raise total bundle budget to 461 KB
The chip hover cards and the self-contained Select styles push the canvas
bundle past the previous 453 KB ceiling. Entry budget is untouched (main.js
stays ~4 KB of the 30 KB allowance); this only moves the total.

Raised deliberately rather than trimming further — the remaining headroom is
~8 KB, so new canvas CSS/JS still needs to stay lean.
2026-07-24 04:48:49 +03:00
DumiFlex da2239dca5 feat(loop): mirror bypass_frames in the ts config type
Adds `bypass_frames: number[]` to ContextLoopConfig + its recovery parse
(sorted, deduped, non-negative ints, out-of-range kept — mirrors the Python
_parse_config). Bumps the js/ gzip budget 450 -> 453 KB: the config field
alone landed 34 bytes over 450, and the headroom covers the loop-bypass UI
added in the following tasks.
2026-07-21 02:38:07 +03:00
DumiFlex 8dcebdcc55 feat(context): quick Hold toggle in the module row context menu
Right-click a wildcard / combine / fixed-values row and "Hold across
run" now flips instance.seed_scope without opening the editor (4 clicks
to 2). The item swaps to "Release hold" when held, mirrors the editor's
base-only rule (disabled with a hint while a loop edit-frame is active),
and is gated to HOLD_KINDS so derivation/constraint rows -- whose
handlers ignore seed_scope -- never see it.

Bumps the extension bundle ceiling 449 -> 450 KB (measured 133 B over).
2026-07-01 03:17:58 +03:00
DumiFlex b33afd8797 fix(context): capture the loop's per-frame seeds so the lock pins the previous roll 2026-06-26 06:43:52 +03:00
DumiFlex 6eb4b730cb fix(context): per-frame enable overrides base enabled (frame_enabled) 2026-06-26 06:43:47 +03:00
DumiFlex 21f285033d feat(context): make the row quick-lock act on the active frame 2026-06-26 06:43:41 +03:00
DumiFlex 4e71e043e7 feat(context): redirect edits into iteration_overrides when a frame is active 2026-06-26 06:43:34 +03:00
DumiFlex fa73750ca0 feat(seedlist): add paste-to-replace locks from copied text 2026-06-26 05:56:54 +03:00
DumiFlex 68910bfc18 feat(seedlist): preview effective base/count/strategy from the wired loop 2026-06-26 05:56:50 +03:00
DumiFlex 43d775ec8f feat(seedlist): per-iteration seeds button + modal 2026-06-26 05:56:47 +03:00
DumiFlex 86eb15cfa3 feat(loop): per-iteration seeds button + modal; shrink iteration-var buttons 2026-06-26 05:56:46 +03:00
DumiFlex 538db06efc feat(constraint): group the canvas matrix axes like the SPA
The ComfyUI instance-modal matrix (MatrixSection) was still flat while the SPA
ConstraintMatrix had grown grouped axes. Bring it to parity:

- Extract the pure grouping logic (order-by-axis, contiguous groups, bucket
  detection, labels, per-axis hue) into a shared components/shared/matrix-axis
  module so the two grids stay identical; refactor the SPA matrix to import it.
- Port MatrixSection: column bands name each target axis, row header chips name
  each multi-tag source axis, a solo axis folds its name into an eyebrow, and
  the leftover run is labelled "uncategorized" (neutral, dashed) — not blank.
  Headers + tag column + group chips + corner freeze while the grid scrolls on
  both axes. All per-instance cell logic (overrides, popover, stranded) is
  unchanged; cells stay keyed by tag name so the reorder is safe.
- ConstraintInstanceModal threads each wildcard's tag_groups into the matrix.
  Cross-context refs resolved only via the preview-resolver cache (which has no
  tag_groups) degrade to flat — the common sibling/chain case groups.
- Bump the extension bundle budget 432 -> 435 KB for the ~1.1 KB net growth.
2026-06-22 15:27:08 +03:00
DumiFlex d8c20728cb fix(extension): wildcard pool MOD dot, subtitle counts, library-aware refs
Three pool/UI fixes. (B) The MOD dot now lights on filter / pick-range / exclude-null changes — the category_filter check ran nonEmptyArr() on a string and silently missed, so toggling the pool filter never marked the instance modified. (C/D) The wildcard card subtitle shows 'X of Y options' when a filter narrows the pool, and '· picks N[-M]' when the instance multi-picks. (F) A nested @{uuid} ref to a live-library wildcard not in the chain no longer shows a false BROKEN REF — the Context node DB-expands its catalog at run, so it resolves; only refs absent from BOTH chain and library still flag (and those already emit unknown_ref in the debug panel). Bumps the extension bundle budget 431->432 KB.
2026-06-18 05:42:34 +03:00
DumiFlex a19e5f4500 Merge remote-tracking branch 'origin/main' into fix/instance-modal-scroll
# Conflicts:
#	scripts/check-bundle-size.mjs
2026-06-18 03:44:54 +03:00
DumiFlex c43bff0488 fix(extension): scroll instance edit modals with pinned header + footer
Every per-instance edit modal (wildcard, fixed-values, derivation, combine, constraint, bundle) now scrolls its body when content overflows, with the header pinned to the top and a sticky dock pinned to the bottom (runtime + footer, or target-reach + footer for constraints, which have no runtime) so Save is always reachable. The long inner lists — options, values, rules, exceptions — each scroll independently with sticky column headers. Constraint's target-reach moves to last. Compact + right-padded the exceptions delete button. Bumps the extension bundle budget 430->431 KB for the added modal CSS.
2026-06-18 03:23:24 +03:00
DumiFlex 0f5166e7d1 fix(manager): recover from rich-text editor crash and persist drafts on unload
RichTextInput rebuilt an index-keyed v-for over a hand-mutated contenteditable when the nested-ref picker stole focus, throwing insertBefore/null and freezing the page. Scope a renderEpoch rebuild to the chip-insert path so typing/backspace is untouched. useEditorDraft now flushes the pending debounced draft on unmount and beforeunload, so a refresh after a freeze no longer loses work; ErrorBoundary tells the user their work was auto-saved. Bumps the extension bundle budget 430->431 KB for the added widget code.
2026-06-18 01:25:24 +03:00
DumiFlex 2eb0e365b7 feat(ts): combineConstraintFactor mirror + shared corpus parity
Add the TypeScript mirror of engine/modules/_constraint_math.py with
identical semantics, validated against the SAME shared corpus
(tests/fixtures/constraint-corpus.json) so Py === TS parity holds. Vitest
covers 2 unit cases (multi-tag multiply, exclude-absorbing) plus the 4
corpus cases (multi-tag->1, multi-pick->6, exclude->EXCLUDE, exception->0.25).

Also bump bundle TOTAL_LIMIT 405 -> 430 KB for the upcoming SP3 constraint
reach UI; entry/per-file limits unchanged.
2026-06-11 06:49:59 +03:00
DumiFlex 8000e1b75c fix(ui): render embed/canvas checkboxes via a scoped WpCheck component
The inline `<span class="wp-check">` controls relied on the global `.wp-check`
rule from manager/styles/tokens.css, which is loaded in the SPA but NOT on the
ComfyUI canvas (settings.ts pulls a hand-picked CSS set; tokens.css is not in
it). So in canvas-mounted instance modals the converted checkboxes rendered as
unstyled, zero-size spans — invisible. (`.opt__check` survived because it is
component-scoped CSS, which ships into every bundle the component renders in.)

Fix: extract the wildcard-option box into `WpCheck.vue` — a shared component
whose `<style scoped>` mirrors `.opt__check` verbatim. Scoped CSS is compiled
into the component chunk, so it travels into the SPA *and* the canvas bundle
with no dependency on any global stylesheet. Route PoolSection, DisabledRules-
Section, SubcategoryFilterPicker, Push/PushBundleToLibraryModal, and
DisplayPlaygroundModal through it.

The SPA keeps its global `.wp-check` (ui/Checkbox.vue consumers) untouched.
Reverts the 405 -> 407 KB budget bump: one shared component is leaner than the
13 inline copies, so the total lands back under 405 KB.
2026-06-10 01:59:33 +03:00
DumiFlex c6baeb8136 refactor(ui): convert remaining native checkboxes to the shared wp-check look
Sweep the leftover browser-native `<input type="checkbox">` controls onto the
inline `wp-check` span so every checkmark box reads as the wildcard-option
style. Bumps the bundle budget 405 -> 407 KB for the added markup (~0.8 KB
gzip); the embed entry stays slim.

Converted (true checkboxes):
- DisabledRulesSection: per-rule disable toggles (onToggle now takes a bool).
- SubcategoryFilterPicker: exclude-null (manager twin of the pool toggle).
- PushToLibraryModal: "propagate to bundles".
- PushBundleToLibraryModal: cascade "restore missing refs".
- DisplayPlaygroundModal: 7 display-pref switches (drop the dead wp-pg__check
  rule; rows become <div> so no orphaned <label> remains).

Intentionally left as-is — these are toggle SWITCHES / custom toggle buttons,
not checkmark boxes: the row-enable sliders (ModuleRow, InjectorRow,
MockModuleCard, BundleHeader), the internal-flag toggle, and seed-lock.

Tests updated to the aria-checked / click contract.
2026-06-10 01:34:05 +03:00
DumiFlex 33e6bd0d34 feat(ui): nested-ref filter popover + chip + in-graph pool editor
Chunk F (src/manager/components): SubcategoryFilterPicker reworked into a text-first boolean-expression editor (insert-at-cursor chips/operators, live reads-as + 'N of M match' count + validateExpression, exclude-null toggle; emits {expr, excludeNull}). RefChip shows a compact funnel indicator + hover tooltip instead of an inline subcat suffix. RichTextInput serializes ref atoms to @{uuid#name:expr!null}.

Chunk G (src/components/context/editors/wildcard): probability.ts isEnabled matches the boolean category_filter expression (parse+matches) + exclude_null. OptionRow renders per-tag CATEGORY chips colored by tag_groups axis. PoolSection: grouped quick pills (or within a group, and across) + an advanced boolean editor + exclude-null toggle.

Foundation/cascade: _shared.ts Instance.category_filter string[]->string + exclude_null; prune.ts drops stale tokens from the expression via subcatExprCascade; ContextWidget tooltip renders the expression string. Bundle-size budget 395->405 KB for the graph editor + shared parser (documented). Full vue-tsc + vitest green.
2026-06-07 03:41:01 +03:00
DumiFlex 51f6793177 test(validators): engine-parity harness to stop validator drift
The community TS validators re-implement the engine's payload shapes by
hand, and drifted repeatedly (fixed_values entries-vs-values, wildcard
is_null omitted) -- each drift surfaced as a publish failure for a user.
This makes the engine the single source of truth.

- scripts/dump_engine_shapes.py: builds one sample payload per module
  subtype, validates each against that subtype's own engine
  Handler.validate_payload (raises if not engine-valid), and writes the
  engine-row shape to src/validators/fixtures/engine-parity/*.json. Run
  it whenever an engine module shape changes; commit the regenerated
  fixtures.
- src/validators/__tests__/engine-parity.test.ts: asserts the STRICT
  community validator accepts every engine-dumped fixture. 5/5 pass.

Now drift fails CI on either side: a sample that no longer matches its
handler fails the generator; a validator that rejects the real shape
fails the parity test. CLAUDE.md schema section documents the loop.
2026-06-06 10:44:01 +03:00
DumiFlex 730721fa1a feat(registry): comfyignore + 21:9 banner + includes + collapsed changelog header
Five tightly-related changes to the registry-publishing path.

.comfyignore
  Drops dev-only + repo-plumbing files from the registry archive
  even though they stay in git: src/ (frontend source), tests/,
  .github/, .husky/, public/images/docs/ (doc screenshots used at
  SPA build time, baked into web/ anyway), vite + eslint + commitlint
  + semantic-release configs, scripts/, repo metadata (CHANGELOG,
  CONTRIBUTING, CODEOWNERS, CODE_OF_CONDUCT, SECURITY). Syntax is
  gitignore-style globs per the Registry spec.

[tool.comfy] in pyproject.toml
  - Banner: 21:9 svg at public/images/comfy-banner.svg served via
    raw.githubusercontent so the listing card on registry.comfy.org
    renders the project banner instead of an empty placeholder.
  - requires-comfyui = ">=0.3.36" — floor that introduces the V3
    io.* schema (io.Schema, io.ComfyNode, @comfytype, ComfyTypeIO)
    the entire wp_nodes/ tree uses. Older runtimes would fail at
    import; the Manager warns instead.
  - includes = ["js", "web"] — both build-output trees are
    gitignored, so without this the registry archive would ship
    without the canvas widgets (js/) or the manager SPA (web/).

public/images/comfy-banner.svg
  Standalone 1400×600 svg (≈2.33:1, matches 21:9). Left half: title +
  underline accent + tagline + module-kind badges (Wildcard / Fixed /
  Combine / Derivation / Constraint, kind-colored). Right half: WP
  Context → emits → resolved $vars → fills → Prompt Assembler flow.
  Faint canvas-grid background so it reads as "this lives in
  ComfyUI" at a glance.

scripts/pack-release.mjs
  Fixes the manager outDir literal: ships web/ (the actual vite
  outDir per vite.config.mts), not the stale web_dist/ name in the
  CLAUDE.md comment. Without this the github-release zip would
  ship without the SPA.

.releaserc.js — collapsed-changelog header / footer partials
  release-notes-generator's writerOpts now wraps the per-commit
  list in a <details> element with a fixed "Wildcard Pipeline
  <version>" + doc/discord/install/issues links header. GitHub
  renders <details> natively in release markdown — readers see the
  TL;DR + 5 useful links above the fold (well under GitHub's
  "Read more" character cutoff) and click to expand the full
  feat/fix/perf log. The header text is also the part most likely
  to stay useful weeks after the release.
2026-05-28 14:09:39 +03:00
DumiFlex 9b91012195 feat(github): publish workflow + issue/PR templates + release-zip + cleaner notes
Drops six pieces of repo plumbing in one go.

publish workflow (.github/workflows/publish.yml)
  Fires on every published GitHub release + manual dispatch. Builds
  the extension + manager SPA, then invokes Comfy-Org/publish-node-action
  with REGISTRY_ACCESS_TOKEN so the ComfyUI Manager listing stays in
  lockstep with semantic-release tags. Without this the README's
  "search Wildcard Pipeline in the ComfyUI Manager" path would never
  see new versions.

issue + PR forms (.github/ISSUE_TEMPLATE/, PULL_REQUEST_TEMPLATE.md)
  - config.yml: blank issues off, three contact links (Discord first,
    Discussions, Wiki).
  - bug_report.yml: structured form with version chip, multi-select
    surface dropdown covering all 10 nodes + 5 SPA areas + engine +
    build, ComfyUI version, OS, Python version, repro, expected vs
    actual, traceback (shell render), workflow JSON (json render),
    WP Debug warnings paste-box.
  - feature_request.yml: problem + proposal + alternatives + scope +
    importance dropdown; nudges toward "describe the underlying
    problem, not just the solution".
  - PULL_REQUEST_TEMPLATE.md: summary, conventional-commit type
    checkboxes mapped to release impact, related-issues line,
    local-verification gate checklist, screenshots prompt for
    visible changes, risk callout box.

release-zip + cleaner notes (.releaserc.js + scripts/pack-release.mjs)
  - Switched commit-analyzer + release-notes-generator to the
    conventionalcommits preset (already a transitive dep, no install).
  - Marked chore/docs/style/test/build/ci as hidden:true so they
    don't render in the GitHub release body. Only feat/fix/perf/
    refactor/revert show. Past releases (v1.8.0) keep their long
    notes; v1.9.0+ ships clean.
  - New scripts/pack-release.mjs zips the deployable file set
    (engine/, wp_nodes/, wp_api/, js/, web_dist/, public/, docs/help/,
    __init__.py, pyproject.toml, README.md, LICENSE) into
    dist/ComfyUI-Wildcard-Pipeline-v<version>.zip.
  - @semantic-release/exec prepareCmd runs the version-stamping
    AND the pack-release.mjs invocation so the zip carries the
    right version in its name.
  - @semantic-release/github assets config attaches the zip to the
    GitHub release page so manual installers can grab a single
    artifact instead of cloning at a specific tag.

Notes on what's NOT changed:
  - ci.yml + release.yml stay as-is — local versions are a strict
    superset of origin's (coverage gate, perf gate, fuzz job,
    bundle-size budget). Release workflow already had the env +
    permissions needed for semantic-release.
  - bundle-size-pr.yml stays (local-only, useful).
2026-05-28 13:42:51 +03:00
DumiFlex ccbf38af99 feat(nodes): add wp_seedlist + split loop config types + wp_ prefix sweep + tooltips
Three coordinated changes to the WP_* node family.

1. New WP_SeedList node
   Emits a list of N derived seeds for a downstream loop (is_output_list).
   Three independent override toggles (override_seed / override_count /
   override_strategy) each mirror the wired Loop's base_seed / count /
   strategy. Connecting loop_config auto-flips override_count +
   override_strategy ON so the natural intent of wiring in a loop gets
   the defaults right; override_seed stays explicit opt-in.
   All emitted seeds are masked to 50 bits via the new MAX_SAFE_SEED
   constant in engine.seed_derive so values sit inside ComfyUI's
   frontend randomize range and copy-paste reproduces identically in a
   standalone sampler.

2. Context Loop type split + downstream Context seed widget lock
   Split WP_CONTEXT_LOOP_CONFIG (wire payload) from a new
   WP_CONTEXT_LOOP_WIDGET (DOM-widget-only TYPE). Loop's own widget
   mounts on the widget type; the wire type has no factory so consumers
   like WP_SeedList render their loop_config socket as a plain input
   instead of being hidden by a fallback-undefined widget.
   New graph walker hasUpstreamLoopOverridingSeed: WP_Context's widget
   glue greys out the local seed stock widget when an upstream Loop has
   override_seed ON, so users read "this value is dead while the Loop
   drives it" without inspecting the Loop config separately.

3. wp_ prefix sweep + tooltips across every WP_* node input
   Custom DOM widget input names now carry the wp_ prefix so canvas
   socket labels read as branded WP widgets: modules -> wp_modules,
   rows -> wp_rows, viewer -> wp_viewer, cleaner -> wp_cleaner,
   context_loop_config -> wp_context_loop_config. Every stock input
   across the node family gets a tooltip describing what it does and
   when to wire it from another node. Tests + comfy_api stub updated.
   CLAUDE.md notes the wp_ convention for future custom widgets.

Bundle size budget bumped 388 -> 395 KB to fit the SeedList Vue chunk
(~4.9 KB net gzip) + override lock UI; entry stays at ~4 KB gzipped.
2026-05-28 12:40:44 +03:00
DumiFlex 2d72fe4345 feat(injector): two-tier socket + general-template row model
Socket rows now scope their optional template to their OWN slot only
(top-to-bottom resolution; other $input_M stay literal). Adds a durable
general-template row (kind: "general") that survives socket churn, is
resolved after all socket rows, and composes from both raw sockets
($input_N) and socket-row bindings ($test). Engine processes socket rows
first then general rows in order; general rows honor binding validation +
the internal flag. Frontend: kind discriminator, "Add template row"
affordance, general-aware reconcile that keeps general rows, inline
binding+template editor with a refs hint, conflict-scanner + docs updates.
2026-05-28 03:39:45 +03:00
DumiFlex 3e404fbd98 revert(size): restore 385 KB budget — docs is in web_dist, not js/
The bundle-size gate measures only the extension bundle (js/). The
Documentation tab lives in the manager SPA (web_dist/), so it does not
affect this budget; the earlier 395 KB bump was unnecessary and is
reverted to keep the extension gate meaningful.
2026-05-27 06:21:36 +03:00
DumiFlex f36ac51d5b chore(size): bump total bundle budget 385 -> 395 KB for docs tab
The in-SPA Documentation tab adds a lazy Docs chunk (registry + doc
primitives + ~26 authored content pages). Hand-authored Vue, no markdown
dep; entry main.js untouched.
2026-05-27 06:07:34 +03:00
DumiFlex df95e81ec6 feat(assembler): round-trip template identity + richer Save/Load modals
Load now persists the picked template's {id,name} on node.properties
(workflow-serialized, survives reload), so Save can offer to update the
exact row the user loaded — the template-side analogue of the bundle
push-to-library identity. Clearing the template drops the ref.

Rebuilds both modals on ModalShell with the library look adapted from
ModulePickerModal / PushToLibraryModal:
- LoadTemplateModal: search + category/tags/favorites filter popover +
  scrollable rows, single-click pick (no kind tabs / multi-select).
- SaveTemplateModal: name/description/category/tags + live preview, with
  a dual footer — "Update existing" (the loaded or same-name row) and
  "Save as new entry" (create, auto-suffixed). Saving rebinds the load
  identity so the next Save defaults to updating that entry.

Bumps the total bundle budget 380 -> 385 KB for the two heavier lazy
modal chunks (~3 KB net gzip; entry untouched).
2026-05-27 02:22:41 +03:00
DumiFlex 34f71d2ecf feat(main): register wp_var_picker widget + raise size budget to 380kb 2026-05-26 15:19:29 +03:00
DumiFlex 9fd9db04a1 feat(cleaner): node, widget, api, migration, mount glue + bundle bump 340->360 2026-05-25 02:28:04 +03:00
DumiFlex 4a69c5e212 fix: cross-node constraint drag + scanner slot allocator + library fallback + save hydrate
- Cross-node drag MOVE-becomes-COPY race: ContextWidget.onDrop sync-claims
  consumedBy before any await so the source widget's dragend reads the
  set value. dragend filters by _uid (idempotent) instead of stale
  srcIdx — fixes the secondary issue where the backup pendingHandoffs
  queue + dragend double-removed an adjacent target wildcard.
- pendingHandoffs queue in drag-store: belt-and-suspenders source-side
  cleanup that runs even when dragend skips (constraint rows inside the
  bundle frame were the trigger).
- Conflict scanner constraint_orphan_target slot allocator: per-target
  sorted local positions + remaining downstream count. Each constraint
  claims smallest local slot > own index, else a downstream slot. Fixes
  duplicated constraints with target downstream cross-node + correctly
  flags when only one local target sits BEFORE the constraint.
- collectDownstreamWildcardUuids: per-instance occurrences (not Set-
  deduped) so duplicated downstream targets each count as a separate
  claimable slot.
- RichTextPreview + ConstraintInstanceModal: cross-Context refs fall
  back to preview-resolver cache (sub_categories, option values,
  options_by_id, hasNullOption) so the canvas modal renders the actual
  wildcard name, sub-cat list, and chip suffix instead of "source"/
  "target" placeholders + stale matrix keys.
- Preview-resolver schema extended with subCategories / optionValues /
  optionsById / hasNullOption populated from embed-bundle response.
- Constraint save hydrate: wp_api modules + bundles endpoints fill
  exception source_value / target_value from source_id / target_id by
  looking up the wildcard's option list before validation. Heals
  library rows that stored only ids.
- TestRunner constraint table uses RichTextPreview for column headers
  + row values so nested @{uuid} refs render as chips.
- Inverted null filter UI: SubcategoryFilterPicker "Exclude null"
  checkbox + RefChip "!null" suffix render.
- Bundle size budget bumped 330 KB to 340 KB.
- Drop stale CHECKPOINT-*.md docs.
2026-05-25 00:17:20 +03:00
DumiFlex 2449c0f407 fix(bundles): exclude entire dragged-range from drop candidates
User reported: dragging a bundle doesn't show an indicator and won't drop below its current position. Root cause: the resolver's "skip the dragged bundle" filter was too narrow — it excluded the dragged bundle's frame and rows whose bundle_origin === draggedUid, but kept the bundle's NESTED descendants (inner bundles + leaves whose bundle_origin matches an inner bundle of the dragged outer) as candidates.

When dragging the outer "hair_style" bundle, the inner "demo_bundle" and its leaves were still drop targets. Pointer over inner's body or its leaves resolved to zones targeting elements that move with the outer — applyDrop would no-op (self-related zone) or produce a misleading indicator at the source.

Fix:
  - New helper `collectMovingBundleUids(draggedUid, bundles)` walks parent_uid descendants and returns the set of bundle uids that move with the dragged range.
  - `classifyAgainst` (top-level walker) skips any bundle in that set AND any row whose bundle_origin is in that set.
  - `classifyWithinBundle` body walk uses the same filter for its directRows iteration.
  - Added a guard at the top of `classifyWithinBundle`: if the bundle the pointer is inside is part of the moving set (e.g. pointer in the inner bundle while dragging the outer), return null entirely. The whole region moves with the drag — no valid drop target.

Bundle-size budget bumped from 312 KB → 316 KB to accommodate the cumulative drag-redesign work (container-scoped resolver + pure applier + indicator SFC + zoom-aware offsetTop walker + moving-range filter). Net add ~1 KB; the +4 KB headroom is for follow-up polish in the same area.

Verified live via Chrome MCP at ComfyUI's 91% canvas zoom:
  - Drag outer header → inner middle: returns null (no valid drop in self-region)
  - Drag outer header → below outer (past everything): bar at end zone
  - dragState clears on dragend ✓
2026-05-21 15:45:43 +03:00
DumiFlex 6103c7c84f feat(canvas-combine): reuse RichTextInput in combine TemplateSection
The canvas combine TemplateSection used a plain textarea + a custom $var insert dropdown. The SPA's RichTextInput (atomic-chip editor) had drifted significantly ahead — chip rendering, $/\@ autocomplete with raw-text-space probe, chipify-on-boundary, click-to-edit, picker-anchored-near-chip — and the canvas was the only surface still showing raw $name text without chip visuals.

TemplateSection now mounts RichTextInput via defineAsyncComponent (chunk-split so the combine modal only pulls it in when opened) with surface="combine", varSuggestions=availableVars, and the existing template_override / library-default collapse semantics. The bespoke insert-var dropdown is gone — the chip editor's $-trigger autocomplete surfaces the same list. Detected pills, preview pane, and resolved-preview pane stay (they're separate read-only panes, not editing surfaces).

Bundle budget bumped 300 → 312 KB to fit the lazy-loaded RichTextInput chunk (~9 KB gzipped). Test rewrites use a small stub for RichTextInput so prop wiring and emit-driven updates can be asserted without booting the async resolver in jsdom.
2026-05-20 16:08:35 +03:00
DumiFlex 207fe08ee8 fix(css): prefix modal-head and modal-template-ctrls selectors with wp-
Brings 25 legacy BEM-style class selectors into compliance with the
extension's wp-* CSS namespace convention (per CLAUDE.md "Extension
isolation"). Renames .cbm__* / .cnm__* / .dvm__* / .fvm__* / .wcm__*
in _modal-head.css and .tpl__* in _modal-template-ctrls.css, plus
every consumer reference across .vue / .ts / .css / .test files.

Also renames the root element classes .wcm and .fvm (referenced from
unscoped display-prefs.css) to .wp-wcm / .wp-fvm, and clears both
entries from the LEGACY_ALLOWLIST in check-css-isolation.mjs now that
they are properly prefixed.

pnpm check:css-isolation now exits 0; all gates clean.
2026-05-18 14:52:51 +03:00