Commit Graph
107 Commits
Author SHA1 Message Date
Dumitru Picalau 4dcb6153f0 feat(ai): recommended pack settings, Open in Pack builder, pack 11/15 and Fixed values fixes (#121)
- The chat's plan card fills the Describe step and waits, so images and settings can be set first.
- Recommended pack settings (no model call) with a Use recommended button on the Settings step.
- Pack 11: model-rolling slots, family-named fixed values, writing-style slots and Pony tags on non-Pony packs are dropped; quality rules join into one; strip_repeats keeps rules on $model_*.
- Pack 15: folded slots are never a self axis; rule words become options after a fill.
- Fixed values panel sends fewer library variables (12-19 s on the 9B, was up to 113 s).
2026-10-26 09:55:58 +01:00
Dumitru PicalauandClaude 941b012f51 fix(ai): capped answer strings, long axes grouped, no second colour in front of a colour wildcard (#119)
The Template panel on an empty library and the pack planner could write one
string on to the output limit; every string in their schemas is capped now.
A slot axis of up to 10 values is grouped, with at least one option per
value kept, so a rule on a value never misses its options. A draw puts no
pick in front of a drawer whose options already name the list's values,
and a drawn or internal list is no longer reported missing from prompts.
The pack eval counts a rule that never fired apart instead of as a break.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-24 23:06:28 +02:00
Dumitru PicalauandClaude dc32cb52f2 feat(ai): pack plans draft again only when needed; slot options come grouped by tag value (#118)
Plan drafts gets When needed (auto, the new default): after the first
draft, one more is asked only when score_plan finds a serious problem
(a relation that named nothing, a review fix other than request words,
a stated rule with no relation, too few prompt slots), and the better
draft is kept. Request gaps also match -ves plurals (elves → elf).

A slot's content call asks its options grouped by one axis (group_axis:
the one a rule narrows on, else one the slot offers, else the first),
at least two per value when the count allows, so every value is covered
by the first call. compile_slot flattens the groups; fill calls stay flat.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-24 21:06:34 +02:00
Dumitru Picalau 72b47fbcd1 fix(ai): the pack plan check holds a plan to its request (#116)
The code lands a share, hold and shown subject itself; a missing rule goes back to the planner once; a cut-off plan is asked again shorter; draw relations are never constraint cells; rules written as text, $$ leads and unasked multi-picks between rules are fixed at compile; the check catches a $name printed as written.
2026-10-24 10:42:10 +02:00
Dumitru Picalau b23241ff7a fix(ai): pack builder settings reach the graph build, rules on drawn lists hold, honest feature scores (#115)
Graph planner settings lines, an accepts-aware dead-end tidy, constraints on drawn lists before their drawers (checked on drawn picks), a repair loop that skips unchanged problems, slot calls with style examples, and a pack eval smoke set. 9B smoke run: 12/15 clean packs vs 6/15 on dev, 15/15 plans compile.
2026-10-24 08:26:57 +02:00
Dumitru Picalau 9aa32718b1 fix(ai): Draft with AI panels answer in seconds on local models, with a panel eval set (#114)
Panels send the thinking off switch on Auto with sized output limits and a thinking watchdog. They list only the variables a request is about. They name wildcard names that two items share, recheck Add for a second concept, stop looping text answers early and ask again for a variable the request names. New scripts/ai_panel_evals.py; the 9B goes from 31/36 to 35/36 checks at 2.4x the speed.
2026-10-24 07:11:15 +02:00
Dumitru Picalau ecb158e4cf feat(ai): short tool list for small models, and drop a 1$$ pick around a lone ref (#112)
Short tool list (Settings › AI assistant › Advanced, off by default): a small model in typed JSON steps gets one line per tool in its instructions, and each tool's arguments and example in the tail of the step that first offers it, once while visible. It is fixed per chat (meta tool_index), so the prompt prefix stays stable. The eval takes --lean.

9B A/B: content tied (55 vs 55 over three pairs) with about 14% fewer prompt tokens per step, so the switch stays off by default for now.

Also drop "{1$$@{…}}" around a lone ref, like the bare braces.
2026-10-24 00:27:52 +02:00
Dumitru Picalau a449b50aa0 fix(ai): the request check's note comes with the first step after the cards (#109)
The 9B spent 2-4 steps after its card on more_tools and opening the card
it had just proposed, and only got the request check's note once it
answered (smoke on #108); on the full library it answered on the last
step, which skipped the note (no step left for the fix). A step after
the cards that changes nothing now carries the note, the note gives
itself the fix step and the answer even past the step cap, and opening
its own card (which can't open until the answer ends) closes open for
the rest of the answer. The smoke set's s-where check also accepts the
switch's own label.
2026-10-23 16:48:34 +02:00
Dumitru Picalau 13d5f819b4 fix(ai): after the request check, offer only the fix; mark claims no card backs (#108)
On an empty library the 9B met the request check's note with more_tools,
opened its own card twice and then claimed the fallback or the
@{Palette:warm} options it never proposed (13 of 20 smoke checks, 17 on
a full library). The step after the note now offers only the cards' own
tools and the ones that add what is missing (request_check.fix_tools),
and a reply that talks about a missing feature anyway says no card has
it yet (request_check.claimed; "no fallback is needed" doesn't count).
A repeated refusal that forces the answer no longer skips the note.
The chat eval's --empty runs every scenario on a migrated empty
library, a new user's first chat.
2026-10-23 13:06:01 +02:00
Dumitru Picalau 9ec21566f3 feat(ai): optional samplers for local and OpenAI-compatible servers (#105)
Settings › AI › Advanced › Sampling sets top_p, top_k, min_p and repeat/presence penalties per provider slot (empty keeps the server's default). They go only to local and OpenAI-compatible servers. The chat eval gains --seed for a fixed sampling seed per scenario and run.
2026-10-23 09:00:47 +02:00
Dumitru Picalau 7ddc7ac1a3 feat(ai): check the cards against the request; close a looping tool (#103)
The request check (engine/ai/request_check.py) nudges once when the answer's cards lack a constraint, only rule, fallback, negatives or tag-filtered ref the request names. A tool repeated MAX_LOOPS times leaves the grammar for the rest of the answer. Page tools come to a task only on page words, and option tags in a wildcard ask no longer load the list tools.
2026-10-23 08:15:28 +02:00
Dumitru Picalau 76e895cf7a feat(ai): narrow small models' grammars; questions get no change tools (#102)
- Typed JSON step grammars name only real items: ids limited to what the edit guard allows, read/run_test/open to what exists, and the tools that usually come next join the shortlist; a server that refuses the enums gets the plain grammar.
- A question or chat gets the reading, page and docs tools but nothing that changes or drafts; a follow-up in a chat that already proposed cards counts as a task.
- The shortlist core is search, read, ask and more_tools; docs, react, variables and run_test join only when the message or the answer calls for them, so first steps offer about ten tools.
- Traces record the message kind; eval runs keep each run's library and card ops, and the smoke checks read compiled refs.
2026-10-23 05:49:47 +02:00
Dumitru Picalau 59ab97f547 feat(ai): feature cards for small models, Type out replies and live thinking (#101)
- Send small and medium models feature cards with the message that needs them, and offer the tools a card names.
- Suggest local models by name only.
- Optional Type out replies (off by default), JSON steps included; the Thinking box streams live and the drawer says when the model sends nothing.
- Let the chat change an item its own card made.
- Evals start from a fresh library; traces record step time, thinking tokens and timeouts.
2026-10-23 04:57:35 +02:00
Dumitru Picalau 1adb9ebedf feat(ai): trace each chat answer and add a small-model smoke set (#100)
Each answer stores what its steps offered the model and what it did, summed by scripts/ai_trace_report.py. scripts/ai_chat_evals.py --set smoke runs ten asks with content checks. A new test fails if a request stops starting with the one before.
2026-10-23 02:52:33 +02:00
Dumitru Picalau 8d0fa337b2 chore: fixes from the 2026-10-21 review (#93)
Shorter release notes, SeedList help, small-model docs; _chat_turn split and pyright in CI; ESLint a11y ratchet and Modal focus trap; tags/models/Image Filter tests; AI LAN warning, web allow expiry, one thinking setting per answer on local models, recommended hints and Reset to recommended (16k context minimum, 32k recommended); Assembler Clear template clears the negative; Vue 3.5.44, SECURITY and Dependabot.
2026-10-22 06:15:13 +02:00
Dumitru Picalau 61baff389f feat(ai): pack rules that choose, a draw relation, picks and model families read off the request (#81)
The pack-sheet round on the graph planner: agree/only sources roll an
accepts axis, fallback counting and tight-rule tips, the draw relation,
picks and model families read off the request, Quality and Negative
slots wired as fixed values and the template negative, Plan drafts best
of three, Danbooru respelling, thinking-off retry for all-reasoning JSON
answers, Reuse what fits on a graph plan, negative-ish names out of the
order, numeric derive text as weights, fills for missing axis values,
one derivation rule per axis, multi-pick rules dropped, request gaps
asked once, overlaps filed on the borrowing pick, the chat claim / defer
nudges, the empty-retag re-ask, and the adult-pack safety pass on every
wired item (request refusal, young-person words, tags and keyed rules
dropped, "mature" out of every negative).
2026-10-21 14:43:44 +02:00
Dumitru Picalau 8bec450749 feat(ai): Pack builder plans a graph and the code wires the pack (#80)
A new Planner setting, Pack graph (the default), has the model write a
small typed graph (slots, axes, relations, template order) and the code
compiles every constraint, derivation, fixed value and the template from
it. Run and settings needs are wired by the code (story beats, sweep axes,
per-family quality, caps, style, rating, one bundle), hand edits are
locked, the plan review stops looping (typed findings, code questions,
narrow per-item Change with AI calls), and the chat gets the 9B fixes
(Tool format Automatic picks JSON steps for small models, XML calls run,
frame tests take numbers, written-out calls still run).
2026-10-21 14:42:39 +02:00
Dumitru Picalau dbcf48a6b5 fix(ai): chat assistant eval and fixes (#79)
One card per idea, run_test finds items however the model names them, small-model rule and constraint shapes are cleaned up, Draft with AI panels use integer-enum ids with a catch-all repair call, Pack builder Fix all no longer loops, and scripts/ai_chat_evals.py runs scripted chats against a real model.
2026-10-19 03:15:23 +02:00
Dumitru Picalau aa5cc4605c fix(import): refuse malformed packs in the manager before install; enforce coverage gates (#76)
Every install path parses with parseCheckedPayload, which checks module payloads and bundle children against the strict validators (loaded lazily) before /import/commit. Validators accept the engine's defaults. CI now evaluates the pytest coverage floor (88) and css-isolation; vitest thresholds ratcheted to 80/66/77/80; js budget 496 KB.
2026-10-17 19:30:00 +02:00
Dumitru Picalau 26df88216f feat(ai): chat round, node negatives, pack builder quality fixes (#69)
Chat round (explain on selection, page starters, reactions, streamed notes, thinking), negatives on node fixed values and combines, Pack builder quality fixes from 31 real-model eval rounds, and small-model support (scoped tools, checked tool arguments on local servers, two-phase steps, fixed prefix shown in Test connection).
2026-10-17 01:23:58 +02:00
Dumitru Picalau df9a19a624 fix: engine rng keying, widget teardown, import validation and connection leak (#72)
Derivation and fixed values modules roll on their own rng stream (seeds through their alternations can resolve differently). DOM widgets tear down when their node is removed (effect scope, cleanups, shared onConnectionsChange wrapper, seed state in WeakMaps, capped topbar rAF loop). /import/commit validates module and bundle child payloads. WP_Context and preview close their library connection; the node path migrates once. Pinned option missing warns. CLAUDE.md says which parts of the schema machine are wired; js budget 473 KB.
2026-10-17 01:07:34 +02:00
Dumitru Picalau f8f49bca7c feat(ai): the assistant knows the nodes, tags and loras; page reading, danbooru lookup and new pack settings (#71)
Node knowledge and canvas variables for every AI feature, local tag/LoRA/embedding lookup, read_result page reading (built in, Crawl4AI or Firecrawl), opt-in Danbooru lookup, dismissable web warnings and "don't ask before web calls". Pack builder reads run style, subject and LoRA inputs from the request into the plan (Runs as), settings keep only pack-wide preferences plus Model; plan review checks stories, LoRA inputs and the model setting; anchor finding fixed; Fix with AI / Explain buttons; docs figure descriptions.
2026-10-14 23:13:55 +02:00
Dumitru Picalau d25c2e4558 feat(manager): split loading mark replaces the plain spinners (#68)
The logo rings slide apart and back while the star dips and turns. Used for busy buttons, the database and maintenance panels, pack builder rows, template, tags and impact loading text, the Community tab while the embed opens, the canvas Load template and Push to library modals, and a boot splash in manager.html. js/ budget raised 468 -> 472 KB.
2026-10-14 20:32:00 +02:00
Claude 016c275fc6 feat(ai): assistant covers fixed values, combines, templates, bundles and the Test Runner
- AI panels in the fixed values, combine and template editors (/wp/api/ai/text/draft)
- chat creates and edits fixed values, combines, templates, bundles, rules and constraints, and runs test stacks
- Test Runner bar: explain the run, fix its warnings, why an option never comes up
- shared authoring guide (syntax, which feature for which job, coherence) for chat and pack builder
- pack builder plans fixed values, vocabularies, per-use settings and inputs; evals score features
- docs, README and release notes updated
2026-10-12 20:18:55 +00:00
Claude b6788e442a feat(ai): pack builder (plan, build, check, install) + offline evals
The Pack builder page plans a whole pack from one description (wildcards,
derivations, combines, constraints, a template), builds each item in its
own request, runs the pack on the engine, rebuilds items the run flags,
and installs everything as one tagged collection plus a bundle with one
undo. scripts/ai_pack_evals.py scores packs against a real model.

Test Runner warnings now keep their detail as data instead of a repr.
2026-10-12 15:31:06 +00:00
Claude 8878ca879c feat(image-filter): details panel in the zoom; loop locks follow frame changes
I (or Details) adds the frame's variable values, seed, other images and
the prompt with each value marked to the zoom. The node now sends each
frame's variables. Lowering a Context Loop's count drops seed locks and
bypasses past it, and a sweep edit moves them with their combination
instead of leaving them on whatever frame lands there. Budget 468 KB.
2026-10-07 20:43:29 +00:00
Claude ebc1ea9978 feat(image-filter): refine step with prompt edits, masks, sweep grid and send to loop
The picker now labels frames with their swept options, seed and prompts,
lays a sweep of two or more wildcards out as a grid, compares two images
in zoom, remembers whether it was left zoomed, and can send the picks back
to the Context Loop (locking kept frames' seeds, bypassing the rest).

In zoom a Refine panel edits the frame's positive/negative prompt and
paints a mask. Edits ride along with the answer: text outputs follow the
edit, an optional clip input re-encodes the conditioning, and painted
masks go to the masks output (alpha of a PNG, resized to the image).

The tab title shows a mark while images wait, and an opt-in setting sends
a desktop notification when the tab is in the background. The "Nothing
picked" option is gone: the picker can't send an empty pick, and the
timeout rule covers the only case left.

Raises the bundle budget 452 -> 464 KB for the larger lazy picker chunk.
2026-10-07 17:49:42 +00:00
Claude 1a76ea9d45 feat(nodes): pick which images go on with WP Image Filter
A new node that pauses the run and lets you pick images in a picker.
Only the picks continue, and every other slot follows them: latent and
masks are sliced, batched CONDITIONING is sliced (pooled_output too),
prompt texts and Contexts are kept or dropped with their frame, and two
MatchType extras carry anything else. Works per frame for Context Loop
lists, sends picks as the same shape or one item per image, can reuse
the last picks or pass everything, and stops only its own branch when
asked. The picker can be minimised without losing picks.

Raises the js/ bundle budget from 440 to 452 KB for the picker glue.
2026-10-07 16:20:18 +00:00
Dumitru Picalau 261acc7635 feat(conflicts): warn when a constraint can rule out every target option (#54)
The canvas now flags a constraint row with "rules out all" when some
source pick leaves its target wildcard with nothing to pick and no
fallback to stand in.

Also fixes the All items list, which stopped at 1000 rows: it now loads
the whole library and pages client-side.
2026-10-07 01:21:51 +02:00
Claude 56047962dd fix(model-info): use the shared row buttons and always re-run the node
The globe, pin and remove buttons now use the row-primitive icon
buttons the Injector and module rows use. The node re-runs every time:
a cached run sends no UI payload, so the widget lost the detected
family after a repeat run.
2026-10-06 05:43:30 +00:00
Claude 68326e855a feat(model-info): keep the model variables out of the assembler unless shown
Each Detected row gets the Injector's globe. All three start internal:
later rules read them, the Assembler and its chips leave them out until
the globe is clicked. The canvas walkers and preview chain follow the
same flags.
2026-10-06 05:31:17 +00:00
Claude dd234875db feat(model-info): give the node a proper widget and drop the string outputs
Detected values with their source and a pin per row replace the
override text boxes; variant rules become editable rows that light up
on a match. The family shows after a run via the node's UI payload.
The node now outputs only its context.
2026-10-06 04:39:42 +00:00
Claude 929cd53652 feat: add a model info node and match-variable wildcards for model-aware pipelines
WP Model Info writes $model_family (from the model's config class),
$model_variant (file-name rules) and $model_name into the Context, reading
the checkpoint name by following its MODEL link back to the loader. Graph
walkers, the assembler preview and WP Debug know the new variables.

A wildcard instance can now match a variable instead of rolling, so a
model wildcard becomes a constraint source. The derivation editor gains a
LoRA by model preset. Docs, node help, README and release notes updated;
bundle budget raised to 432 KB.
2026-10-06 03:53:50 +00:00
Claude e00f38b6ff feat(context-loop): sweep mode runs every combination of chosen wildcards
Pick downstream wildcards in the Context Loop's new Sweep panel and the
loop runs the cartesian product of their options, one frame each, up to a
limit. Each frame pins the swept wildcards through the existing pinned
path; 'Hold other picks' keeps unswept modules on the frame-0 seed.
Raises the bundle budget 424 -> 428 KB for the panel.
2026-10-05 19:12:01 +00:00
Claude a11ceaac1c feat(wildcard): fallback option used when nothing else is left to pick
A wildcard option can be marked as the fallback. It never takes part in a
normal draw and is used only when constraints (an Only rule included), a
filter or zero weights leave nothing else to pick, instead of resolving to
an empty string. Works for top-level wildcards and nested @{} refs, and
emits an info-level `fallback_used` note in place of
`constraint_excludes_all_options`.

- engine: engine/modules/_fallback.py shared by the handler and
  ref_option_pool; payload validation allows one boolean fallback on a
  non-null option
- schema 9 (additive): FALLBACK_SCHEMA_VERSION stamped in TS and Python,
  MAX_KNOWN_SCHEMA_VERSION 9, strict validator keeps the flag, parity
  fixture regenerated
- library editor: "set as fallback" on an option's probability cell, a
  fallback chip, its weight disabled and excluded from the shares
- canvas: FALLBACK badge on the option row, no share in probabilities
- WP Debug: "Fallback used" label and step fact
- docs, release note, CLAUDE.md
- bundle budget raised 420 -> 424 KB (maintainer approved)
2026-10-02 16:10:31 +00:00
Claude add6506225 feat(derivation): run several actions per branch and read negatives with $var.neg
A derivation branch (and its ELSE) now runs its action and then each
entry of an optional extra_actions list, in order; each action sees the
previous one's write. The library editor gets "+ And" rows, the canvas
modal shows every action and gives each its own value override (keyed
"bi.K" / "else.K"), and WP Debug lists every action a branch ran.

$name.neg / $name.K.neg reads the negatives a variable carries. It rides
the accessor grammar's axis slot, so "neg" is reserved as an accepts
group name. Reading .neg is not a read of the variable, so it carries no
negatives along. Works in values, combines, the Assembler's negative
template and conditions; autocomplete offers it after the dot.

Both stamp schema v8. Cascade fixers and scans now walk the real
derivation action shape (action, extra_actions, else) instead of a stale
"actions" list, and read-only previews keep a var chip's accessor.
2026-09-30 15:33:01 +00:00
Claude 4cd180ce86 feat(schema): stamp and validate schema v8 negatives
MAX_KNOWN_SCHEMA_VERSION goes to 8 with a content stamp for any non-empty
`negative` or an "Add to negative" derivation action, mirrored in the
Python exporter. The strict validators keep the new fields, the parity
fixtures carry one of each, and the payload types learn the field and the
`negative` derivation mode.
2026-09-30 04:22:42 +00:00
Claude d6e1a9c2d6 perf(extension): emit lazy chunks as .mjs so ComfyUI stops importing them all
ComfyUI lists every **/*.js under WEB_DIRECTORY in /api/extensions and
the frontend imports each one, so every lazy chunk loaded on every page,
even with no WP node on the canvas (62 files, 1.73 MB raw). Only main.js
keeps .js now; an empty canvas loads 42 files, 576 KB raw.

The size gate counts .mjs and reports it under the old .js names so base
and head still diff. The stale-asset prune matches .mjs, and on the first
start after updating it deletes the old .js chunks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137To3tFTPKen2wYqEo3wme
2026-09-26 01:03:38 +00:00
DumiFlex 934f7dc92d fix(axes): drive constraints by the rolled tag, fix the assembler read, preserve on import
An accepts axis rolls one tag per pick (read as $var.AXIS), but a constraint
sourced from it folded over the option's whole menu instead of that rolled tag,
so $outfit.SHOES and the constrained $shoes could disagree. The source now
sends only the rolled winner; a target accepts axis OR-folds so a multi-tag
option survives when any alternative is allowed, then its own roll is pinned to
the allowed tag -- $source.AXIS == $target.AXIS under a diagonal. Classify
groups are unchanged (multiply).

The assembler also rendered $var.AXIS empty: __wp_axes__ crosses the socket on
ContextPayload.internals but the node only merged the internal-flag map; it now
merges the whole carve-out so the accessor resolves.

Import: the community validators silently dropped tag_group_kinds (and the
constraint target_select) because the lenient payload object never listed them,
degrading a shared accepts axis to classify. Both are now in the v2/v1
validators + the engine-parity fixtures, with preservation tests. Engine math
mirrored in TS via the shared corpus.
2026-08-20 23:58:44 +03:00
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