Two pieces of the "port v4's exact UI onto v5's backend" plan:
- core/contract.py: new optional ModuleSpec.category field (closed set:
continuity, guidance, conditioning, sampling, post, system), validated in
core/schema.py, threaded through core/registry.py's FIELDS and
to_manifest(). Presentation grouping, independent of STAGE (execution
order) -- a module can be stage="sampling" and category="guidance" at
once. Only the 4 existing modules that actually render a settings panel
(alg, sharpen, appearance, full_control) got a category assigned; the
other 12 have none to categorize (no SETTINGS block) and stay
uncategorised, which is valid.
- v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied
verbatim into app/legacy/. core/routes.py's "/" route now serves
legacy/index.html instead of the composer entry point; a <base> tag in
that file resolves its own relative script/link tags against
/funpack/app/legacy/ regardless of what URL it's served at, so every
file moved as a unit with zero per-file path edits. Verified live: all
61 assets load 200 OK, the real v4 onboarding screen renders pixel-for-
pixel (it's the actual DOM/CSS, not a reimplementation), and several v4
API calls (/api/media, /api/projects, /api/git/status) already succeed
against v5's real backend by coincidence of shared route naming -- the
rest 404/405 as expected, since api.js's ~90 functions haven't been
rewired to v5's actual routes yet (next piece of this plan).
The old composer entry point (app/index.html, app/composer/**) stays in
the tree, unreferenced, until a later cleanup pass once nothing imports it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One wide modal, a card per group, and a group that opens into a node list
beside that node/'s parameters, saved a group at a time. Groups travel on the
slot, so a pipeline keeps its own arrangement and a new group is a slot
claiming a name nobody used.
Values are drafted until Save; adding, removing, replacing and moving a node
go to the server as they happen, because only it can say whether the result
still wires up.
An unset dropdown reads "not set" rather than showing the first file as though
it had been chosen -- v4 recorded that trap, and picking the entry the box
already shows fires no change, so the one click that looks like the fix does
nothing.
New: region.stack, a frame header, a modal footer that can be refilled.
Not yet: wiring. A socket says so where a control would be.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I committed the widget layer without running the full suite and broke
twenty-three tests in files that have nothing to do with it.
The registry caches its scan for the life of the process. A route test
redirects MODULES_DIR at a temp directory holding one fake module, and my new
test called install_into inside that -- which scans, and caches the temp
directory as the installed set. Every later test then ran against an empty
registry: the sampling tests found no modules to install, so a modifier that
should have been applied was not, and the failure surfaced as an assertion about
pre_cfg hooks three directories away.
The test needed a node, not FunPack's nodes, and KSampler is one. The fixture
that redirects the directory now also puts the cache back, because whoever moves
it owns restoring it, and there is a test for that -- the failure it prevents is
unrecognisable from where it lands.
graph.py reads an input's TYPE, which is all a wire needs. Editing a value needs
the rest -- the choices a combo offers, the bounds on a number, whether the box
is multiline -- so that is asked separately rather than widening the question
graph.py asks. Read from ComfyUI's own INPUT_TYPES, so a node written for
ComfyUI is editable here without knowing anything about FunPack.
Caught while looking at the real output rather than after committing: deciding
"a socket is upper case" is wrong, because STRING is upper case -- every prompt
box in the pipeline came back as something nobody could type in. The honest rule
is ComfyUI's own primitive set, plus forceInput, which is a short list core
holds on purpose.
/funpack/api/nodes answers by name. ComfyUI's /object_info returns every
installed node, which on a machine with a few packs is megabytes for a question
about a dozen; a node nobody installed comes back as null rather than missing,
so "this slot points at something absent" reads differently from "I did not ask".
The dev server now registers ComfyUI's nodes and FunPack's own, through the same
function the tests use. Without it the pipeline was fourteen "there is no node
called X installed" and nothing to edit -- so the one thing the dev server exists
for could not reach the half of the app that matters. It now reports the three
problems that are real: no model, no CLIP, no VAE chosen.
routes.py had no coverage. register() now takes the route table so the real
routes mount on a throwaway app.
The tests write the request line onto the socket instead of using aiohttp's
TestClient, which normalises the URL before sending -- ask it for
modules/%2e%2e/app/x.css and the server receives a plain app/x.css, so the test
looks like a routing bypass when it is the client tidying up. On the wire the
modules route does match, dots intact, and resolve() refuses it.
Splitting the aiohttp and PromptServer imports: one try around both nulled
web whenever ComfyUI's server module was absent, which is every test run.