Commit Graph
44 Commits
Author SHA1 Message Date
malkuthro 7b00a551d9 feat(publish): independent output type for published setups; fix(pipeline): auto-versioning re-runs each queue (#267)
Publish: Koolook_PublishOutput output_mode/input_switch/switch; setupSurface.app.outputSwitch; routers auto-bound as the output selector; unwired output types hidden; 'Same as input' offered only when every input type has a wired writer; result path follows the output switch. Pipeline: EasyAIPipeline IS_CHANGED keeps version=auto/next perpetually dirty so the vNNN disk scan re-runs every queue; literal versions stay cacheable.
2026-07-19 23:34:30 +02:00
040b30733d feat(matte): first-party mask-guided one-step video matting nodes (#269)
* feat(matte): first-party mask-guided one-step video matting nodes

Adds matte/ — an independent implementation of the published VideoMaMa
one-step SVD method (matte/pipeline.py) plus Koolook's temporal windowing,
subject-focused crop/stitch, resolution control (/64, shortest-side) and
auto OOM resolution-backoff. Nodes: KoolookMatteLoader, KoolookMatteSampler,
KoolookMatteFocusCrop, KoolookMatteFocusStitch.

Registered via the existing guarded _merge_node_group("Koolook Matte",
".matte"), so a missing diffusers disables only these nodes. diffusers is
already pulled in by the radiance/whatdreamscost forks, so no new dependency.

Loads (never bundles) the VideoMaMa UNet (CC-BY-NC) + SVD VAE at runtime;
matte/NOTICE records the model licenses and user responsibility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(dev-sync): include matte/ in RUNTIME_PATHS

So `sync_to_dev` copies the new matte node package into the live ComfyUI
install alongside the rest of the koolook pack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): raw cond latents; fp16 default; preserve checkpoints on dev-sync

pipeline.py: _encode_latents must NOT divide by vae scaling_factor. The
VideoMaMa UNet is trained on raw VAE latents (matching okdalto's
VideoInferencePipeline, which multiplies by scaling_factor then divides it
back out -> net x1). Dividing over-scaled the conditioning ~5.5x and produced
a systematically blurry matte on every frame. Verified 5/5 vs the desktop /
vanilla reference on identical single-frame inputs.

nodes.py: harden the loader precision fallback so fp16 is the true default -
only an explicit "bf16" opts out; a stale "auto" widget now resolves to fp16
instead of bf16.

sync_to_dev.py: the rmtree+copytree mirror deleted matte/checkpoints/ (the
gitignored ~10GB SVD+VideoMaMa weights) on every matte sync, forcing a
re-download. Add PRESERVE_IN_DEST + selective _clean_dest so runtime-
downloaded model data survives a sync.

docs: add docs/maintainers/matte-pipeline.md (latent-scaling invariant,
black-output causes, fidelity levers, single-frame + vanilla-oracle debug
loop, checkpoint preservation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): address PR #269 review

- license: align matte to the repo's GPL-3.0 (was self-contradictory: claimed
  "MIT licensed - see LICENSE" while LICENSE is GPL-3.0). pipeline.py header,
  NOTICE, and the maintainer doc updated. NOTICE still credits VideoMaMa
  (CC BY-NC) + SVD (Stability Community License).
- nodes.py _ensure_2d_mask: squeeze first, then handle [H,W,C]/[C,H,W]
  explicitly and RAISE on ambiguous shapes -- previously [H,W,3] silently
  became [W,3] (wrong geometry).
- nodes.py OOM backoff: catch RuntimeError (OutOfMemoryError subclasses it, but
  cuBLAS/cuDNN/older paths raise plain "RuntimeError: CUDA out of memory"); the
  existing message guard still filters non-OOM errors.
- nodes.py: warn when the guide mask is empty instead of silently falling back
  to full-frame.
- sync_to_dev.py: PRESERVE_IN_DEST is now an exact relative path
  ("matte/checkpoints"), not a bare name matched at any depth; handle symlinks
  before is_dir() in _clean_dest so a symlinked dir isn't recursed/rmtree'd.
- CHANGELOG: add the Koolook Matte nodes under [Unreleased] with the
  non-commercial model-license caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): address PR #269 re-review

- sync_to_dev.py [BLOCKING]: a top-level symlinked dest (e.g. target/matte -> X)
  was passed straight into _clean_dest()/copytree(), which follow the link and
  can mutate files outside the target. Now unlink a symlinked dest and own the
  path as a real dir (checked before exists(), which follows symlinks). Verified
  with a repro: the symlink target's file survives.
- nodes.py: force float32 on a CPU fallback (CUDA unavailable). Half precision
  is unreliable on CPU; fp16 kernels would error. Warn that CPU is very slow.
- docs: correct the preserved-path guidance to the target-relative
  "matte/checkpoints" (was a bare "checkpoints" name); reword "near-verbatim
  reimplementation" -> "independent implementation targeting behavioural parity"
  to match the NOTICE/pipeline framing.
- nodes.py: also warn on a degenerate (<8px) guide-mask region before the
  full-frame fallback (was silent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): address PR #269 re-review (CI red + FocusCrop frame check)

- nodes.py: KoolookMatteFocusCrop now validates images vs coarse_mask frame
  counts (raises like KoolookMatteSampler.run_inference). A mismatch previously
  made cropped_images/cropped_mask diverge and silently drop/misalign frames at
  FocusStitch.
- pipeline.py: split "vae.to('cpu'); _clear_cache(...)" onto two lines (Ruff
  E702 was failing Lint & Smoke).
- constraints-test.txt: bump pillow 12.2.0 -> 12.3.0 (pip-audit CVE, fixed in
  12.3.0). Pillow is a leaf dep so the closure is unaffected; CI re-audits.
- README: note the Koolook Matte nodes need diffusers (already via the LTX /
  Radiance forks) and huggingface_hub for runtime weight downloads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:03:12 +02:00
ba39cb3658 Claude/goofy faraday d23e98 (#255)
* refactor(nodes): drop keyframe_batch alias from Easy Image Batch

The keyframe_batch input was a deprecated alias of keyframes_insert,
added in 0.4.1 during PR #246 review to keep pre-rename workflows
loading. The old name only ever existed in unreleased dev builds before
the rename, so no published workflow depends on it and the alias only
cluttered the node with a second IMAGE input.

Remove the input, its parameter, and the alias-routing fallback; the
node now exposes a single keyframes_insert insert input. Tests and the
behavior spec updated; CHANGELOG notes the removal under [Unreleased].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(policy): make backward compatibility opt-in for Koolook nodes

Invert the default: backward compatibility is no longer a standing
constraint for Koolook-created nodes (the root k_*.py custom nodes).
By default, make the cleanest change — rename/drop/reorder IDs, inputs,
and outputs freely, no deprecation aliases or _v2 suffixes. Compatibility
with saved workflows is only preserved when the maintainer explicitly
says "check backward compatibility".

Forks are explicitly excluded: anything under forks/ (including the
Koolook-original nodes exposed via SKIP_VERSION_SUFFIX) keeps full
back-compat discipline by default, no request needed. The
__koolook_vX_Y_Z collision suffix always applies.

Scoped across the binding instruction files and skills: CLAUDE.md,
copilot-instructions, node-versioning.md, versioning.md, and the
preflight-release / add-external-fork / tdd skills. The preflight
workflows check is reframed as a drift detector for Koolook IDs and a
hard back-compat gate for fork IDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: clarify keyframe_batch alias removal

* fix: retain keyframe_batch compatibility alias

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:16:38 +02:00
472960b919 docs(setups): add publish -> run loop architecture overview (#254)
* docs(setups): add publish -> run loop architecture overview

Entry-point doc that ties the existing published-setup docs together:
the loop in plain terms (Mermaid), a technical map of frontend JS ->
Koolook aiohttp routes/handlers -> ComfyUI engine, and the contract /
schema requirements. All file paths and symbols verified against source.

Diagrams use Mermaid per docs/maintainers/conventions.md. Linked from
the maintainer docs index as the "read this first" entry for the group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(setups): fix publish-run loop references

* docs: codify review-fix push rule

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:04:12 +02:00
malkuthro cb19c375c9 feat(ci): tag-triggered release automation (publish + GitHub Release) (#252)
Push a semver tag (vX.Y.Z) at the squash-merge commit on main to publish to the Comfy Registry and create the matching GitHub Release from the CHANGELOG (only on a successful publish). Pre-publish guard refuses any tag that is not on main (merge-base --is-ancestor); per-job least-privilege permissions. publish.yml kept as a manual fallback. Docs synced: releasing.md + release_checklist.md.
2026-06-19 08:49:40 +02:00
6f158c9020 fix(nodes): Easy_LoadVideo empty-branch degrade (bundles #243 publish-tab cleanup) (#244)
* docs(setups): add setup runner quickstart runbook

Standalone reference for opening the published-setup mockup frontend
(setup runner simulator): the `<comfy-address>/koolook/setup_runner_simulator.html`
URL pattern, the live-registry vs setup-file (inspect-only) load paths,
and demo mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sidebar): close throwaway workflow tabs left by publish capture

Publishing a setup captures ComfyUI's API-format prompt by loading the
graph onto the canvas (graphToPrompt) and reloading the prior canvas.
In current Comfy each loadGraphData spawns an "Unsaved Workflow" tab, so
every publish left two stray tabs behind.

captureWorkflowApiPrompt now snapshots the open workflows before capture
and, afterward, re-activates the user's original tab and closes whatever
the capture opened, via app.extensionManager.workflow. All of it is
guarded and best-effort: the workflow-store API is Comfy-version
specific, so if it is absent capture still succeeds and behavior falls
back to today (no crash, no broken publish).

Covered by a source-pattern test. Needs live verification against the
1.45 frontend (dev-sync) to confirm the closeWorkflow API shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nodes): Easy_LoadVideo degrades to empty on an unselected folder branch

When an upstream mode switch routes a path into Easy_LoadVideo, both the
EXR sequence loader and the video loader execute eagerly. On the EXR
branch the video loader was handed a folder (no filename) and raised
ValueError, crashing the whole prompt -- even though that branch is
discarded downstream. Lenient sequence loaders just ignore a wrong-type
path; the video loader was the only one that hard-failed.

_compose_input_video_path now returns EMPTY_BRANCH_SENTINEL instead of
raising when input_path resolves to an existing directory. load_video
turns that into a correctly-shaped empty result (IMAGE zeros,
frame_count 0, empty AUDIO, empty video_info) matching VHS_LoadVideoPath
returns, so the dead branch no longer crashes the prompt. VALIDATE_INPUTS
and IS_CHANGED handle the sentinel too. A path that does not exist is
still a genuine mistake and keeps raising.

torch is imported lazily in the empty-result builder so the path logic
stays importable/testable without torch.

Covered by unit tests for the sentinel (absolute + relative dirs), the
still-raises case, and a load_video integration test that asserts the
empty result and that the loader never runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(setups): expose prompt as an always-on field in the publish frontend

The publish surface modelled `prompt` as a 4th source mode (mutually
exclusive with EXR/QT/Img) and hid it (visible:false), so a published
setup couldn't offer "pick a source AND describe the shot". This makes
prompt a standalone, always-on input.

- published_surface.js: prompt becomes visible + standalone + multiline,
  rendered independently of the source switch; the author's prompt-widget
  text is surfaced as a `placeholder` hint (submitted default is empty so
  an untouched hint is never sent), with a `help` line. The switch
  excludes standalone fields from its source options.
- setup_runner_simulator.js: renders always-on standalone inputs alongside
  the selected source (textarea + placeholder + help), includes them in the
  run payload, and updates the demo setup.
- setup_runner_simulator.html: .field-help styling.
- contract doc: documents the always-on prompt behaviour.
- launch.json: portability fix (hardcoded C:\Python313 -> python).

Contract test updated + a new test asserting prompt is always-on, carries
the placeholder, and is excluded from the switch source options (runs on
CI where Node is available). Behaviour verified in the runner simulator
(demo mode) in a browser: textarea + hint, EXR/QT/Img-only switch, prompt
stays visible across source changes, payload includes the typed prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): mirror always-on prompt field in the Python publish inference

The publish surface is inferred server-side in Python (koolook_setups.py
_infer_app_surface, called by publishSetup), which overrides the client.
The earlier change only updated the JS (published_surface.js), so
re-publishing still produced the old surface with a hidden prompt.

Mirror the JS here: prompt is a standalone, always-on input (visible +
multiline) with the author's prompt-widget text as a placeholder hint and
an empty submitted default; it is excluded from the source switch options.
Keeps koolook_setups.py and published_surface.js in sync.

Registry test updated to the new shape. Server + publish-contract suites
green (115); full non-sidebar suite green (530).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): keep the runner simulator polling through long renders

The poller used a fixed 30s total budget, so a multi-minute render (e.g.
428s) timed out mid-run with "Timed out waiting for run" even though
ComfyUI was still rendering and the run later landed as succeeded.

timeoutMs is now a "no-progress" window: while ComfyUI keeps reporting the
run as queued/running, the deadline is pushed forward, so a long-but-active
render never spuriously times out; only a stalled/vanished run gives up.
Default raised 30s -> 120s. The run status line shows elapsed seconds while
active so a long wait visibly ticks instead of looking frozen.

Happy path (queued -> running -> succeeded) unchanged; verified in the
runner simulator (demo mode) in a browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): serve simulator assets with no-store so dev-syncs take effect

The runner-simulator HTML/JS route returned no cache headers, so browsers
cached the assets and kept running stale JS after a dev-sync -- e.g. the
old 30s poller persisted client-side even though the server was serving
the long-render fix, which looked like "the fix didn't land". Serve these
dev-synced assets with Cache-Control: no-store so a normal reload always
picks up the current code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(setups): add live run/poll smoke check for the published-setup API

tools/check_setup_run.py queues a published setup through the same routes
the runner simulator uses and polls the run endpoint with the same
deadline-extension logic, printing the status timeline. Lets us self-verify
end-to-end run tracking against a live ComfyUI (e.g. "does a long render
reach succeeded?") without driving the browser UI.

Used it to confirm the run/poll server flow is correct (terminal status
detected in ~2s; run-000001 reported succeeded), isolating the simulator's
earlier "Timed out" to stale browser cache rather than a server bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Koolook_NextVersion auto-detects the next output version

Adds filesystem-aware version auto-detection so a re-run lands on a fresh
version instead of failing with "Output file already exists and overwrite
is disabled" (the real EasyAIPipeline failure seen on the RMGB setup).

- koolook_versioning.next_version_token(directory, name, ...): scans for
  existing <name>_vNNN files AND sequence-folders (handles name_v002.png,
  name_v002.0001.exr, name_v003/), returns highest+1 zero-padded; missing/
  empty dir -> start (v001); exact base-name match so other shots never
  bump this one; empty name matches bare vNNN; configurable prefix/padding.
- Koolook_NextVersion node (Koolook/Publish): folder + name -> next version
  token. Wire once into both the EXR and video version inputs so branches
  stay in sync. IS_CHANGED returns NaN to re-scan every run.

Tested: helper edge cases + node registration/run. Preflight clean (20
node IDs). Full non-sidebar suite green (539).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): 'auto' version keyword for EasyAIPipeline + Easy_VideoCombine

No new node or wiring needed: type 'auto' (or 'next') in the version field
and the node scans its own output folder for existing <name>_vNNN entries
and writes the next free version -- fixing "Output file already exists and
overwrite is disabled" without manual version bumps.

- koolook_versioning.is_auto_version(): detects the auto/next keyword.
- EasyAIPipeline (build_pipeline_outputs): on auto, scans the version-folder
  parent (base, or base/shot/method when subfolders are on) for vNNN dirs and
  picks the next; version computed after path segments so the scan dir is known.
- Easy_VideoCombine: on auto, resolves the token after the output dir + name
  root are composed, scanning that dir for <name>_vNNN outputs.
- Non-auto/typed/wired tokens and disable_versioning are unchanged.

Tests: is_auto_version keywords + EasyAIPipeline auto (v003 after v001/v002,
v001 first run). Full non-sidebar suite green (542), preflight clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(nodes): drop redundant Koolook_NextVersion node

The 'auto' version keyword on EasyAIPipeline + Easy_VideoCombine covers
auto-detection without any new node or wiring, so the standalone
Koolook_NextVersion node is redundant. Remove it (and its node test) to keep
the node list clean; the shared next_version_token helper stays (the 'auto'
keyword uses it).

Node-ID count back to 19; full non-sidebar suite green (541), preflight clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tools): satisfy security gate for setup smoke check

* fix(pr244): address verified review findings

* fix(video): fail if relative auto-version scan root is unavailable

---------

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
2026-06-19 00:10:59 +02:00
malkuthro e165c96fcc fix(ci): enforce dependency audit gate (#247) 2026-06-18 19:40:31 +02:00
malkuthroandClaude Opus 4.8 ae2480eee9 feat(setups): publish success feedback + published-setup discoverability (#227) (#239)
* feat(setups): publish success feedback + published-setup discoverability (#227)

Post-publish feedback (issue #227 Part 1):
- POST /koolook/api/setups now returns the on-disk storagePath
- new POST /koolook/api/setups/reveal opens the published-setups folder
  (shared _open_dir_in_file_manager helper; the snapshot reveal route was
  refactored onto it, behaviour-identical)
- the publish modal renders a success card reusing the snapshot Save dialog's
  "Saved to ... Open folder" language (Open folder / Copy path / Close)
  instead of closing silently; a draft publish still gets a confirmation

Published discoverability (reuses the tag system, no new schema):
- publishing auto-tags the source workflow `published` -> a "published" badge
  in the Workflows tree + the Tags-section "published" pool
- a new "P" toggle in the sidebar Tools row prunes the Workflows tree to
  published-only, folders force-expanded, so setups show in their origin
  structure; toggling off restores the full tree
- workflowHasTag() shared helper (unit-tested)

Deferred, still open on #227: separate destination/library setting (Part 2),
relocating the storage folder beside the snapshot library, and clearing the
filter when entering Compare mode.

Tests: +server (storagePath, reveal route happy + 404), +client (reveal
client, buildPublishSuccessView view-model), +sidebar (workflowHasTag).
Full suite: 610 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(setups): store published setups beside the snapshot library (#227)

- the published-setup registry path now derives from the configured snapshot
  library (`_configured_dir().parent / koolook-published-setups/setups.json`)
  instead of a fixed user-dir folder, so it follows libraryPath /
  KFORGELABS_PRESETS. The default config resolves to the same path as before
  (target == legacy), so existing default installs are unchanged.
- non-destructive one-time migration: copy a legacy user-dir registry to the
  new location when the new one is absent, leaving the original in place.
- sidebar: clear the "P" published filter when entering Compare mode so it
  doesn't render both compare columns filtered with no toggle to undo it.

Tests: +tests/server/test_published_setups_relocation.py (path beside library;
migration copy / no-op-when-present / no-op-when-no-legacy). Full suite: 614
passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(tests): remove unused pathlib.Path import (ruff F401)

The relocation test only uses the `tmp_path` fixture, not `Path` directly, so
`from pathlib import Path` was unused and failed the Lint & Smoke (ruff) gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(setups): address PR #239 review — migration masking + tag persist

- HIGH: a failed legacy->new migration no longer masks real setups with the
  bundled sample. The default registry keeps the legacy file as its primary
  storage when migration could not produce the new file, so stranded data
  stays visible. (+2 tests: keeps-legacy-on-copy-failure / uses-new-on-success)
- MEDIUM/LOW: the publish flow now awaits the `published`-tag persist so the
  success card reflects a committed tag (persist failures already surface via
  persistMutation's critical toast), and warns when the source workflow could
  not be tagged (vanished mid-publish) rather than on a harmless republish.

Full suite: 616 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(setups): harden migration against partial/corrupt relocation target

Addresses the remaining HIGH from the PR #239 consolidated re-review: a
zero-byte/partial/corrupt setups.json at the relocated path could shadow the
intact legacy registry (existence was treated as proof of migration).

- migration now copies via temp file + atomic os.replace (the same pattern
  FileSetupStorage.save_setups uses), so an interrupted copy can never leave
  a partial setups.json at the target; failed-copy temp files are cleaned up.
- primary selection is corruption-aware: when the relocated file exists but
  does not parse as a setups document while the legacy file is intact, the
  registry keeps serving the legacy file and logs a loud diagnostic. The
  unreadable file is left untouched (never overwritten) as evidence. A valid
  relocated file stays authoritative; if both are corrupt, the relocated path
  stays primary so storage diagnostics point at the current location.

Tests (+4): corrupt-target-prefers-intact-legacy (file left untouched),
valid-target-stays-authoritative, both-corrupt-keeps-target, and
interrupted-copy-leaves-no-partial-target-and-no-tmp-litter.

Full suite: 620 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:54:12 +02:00
malkuthro 91cee1daae docs(setups): add external frontend kit (#236)
External frontend kit handoff for published-setup consumers; refreshes full-circle/status docs and neutralizes stale current-PR apiPrompt wording per review #4485848881.
2026-06-12 16:43:51 +02:00
malkuthroandClaude Fable 5 c6a155a033 fix(web): generation-agnostic Comfy draft-quota guard (#237)
* fix(web): generation-agnostic Comfy draft-quota guard

"Failed to save workflow draft" toasts returned on ComfyUI frontend
1.44+ because draft storage moved to new localStorage key names
(per-workspace V1 keys, then a V2 index+payload scheme) that the old
exact-name guard embedded in ltx_director.js never matched - and the
1.44 V2 layer latches a session-wide storage-unavailable flag after one
failed save+evict cycle, re-toasting on every edit until reload.

Move the guard to its own global extension, web/koolook_draft_guard.js:

- match draft keys by prefix (Comfy.Workflow.Draft) across all known
  generations, with unknown future keys as last-resort evictions
- rescue quota failures inside localStorage.setItem (evict the oldest
  draft across generations, retry) before Comfy's own catch sees the
  error, so the save succeeds and the latch never trips
- boot prune: drop the suffixed-V1 dead weight the frontend's V1->V2
  migration never deletes, scope corrupt-key cleanup to the offending
  key, drop oversized entries, enforce a total draft budget
- ship the guard with dev-sync-audio so a scoped sync cannot strand a
  dev install guard-less

Behavior is covered by tests/js/test_draft_guard.mjs (12 scenarios,
wired into CI next to the existing node smoke test); static integration
contracts live in tests/sidebar/test_draft_guard.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): address deep-review findings on the draft guard

- latch the "still full" hint toast once per session so a genuinely
  full origin cannot re-toast on repeated failing autosave attempts;
  regression assertion added to the exhaustion scenario
- document the bounded-eviction termination contract and the
  post-rethrow storage-unavailable latch at the wrapper, plus the
  budget's V1-blob-only (1.5M) -> all-generations (2M) scope widening
  (also now stated in the CHANGELOG entry)
- version-robust wording for the suffixed-V1 cleanup rationale: the
  shipped 1.44 migration leaves the keys behind (verified against the
  installed 1.44.19 dist - no removeItem exists there), later
  frontends clean up on successful migration, and an interrupted
  migration can leak the keys on any version
- comment the intentional best-effort swallow in the toast helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:29:25 +02:00
malkuthro 1732030175 fix(setups): resolve switch-selected run results (#235)
* fix(setups): resolve switch-selected run results

* fix(setups): harden switch result reporting

* fix(setups): reject stale execution map writers
2026-06-09 00:27:45 +02:00
malkuthro c3c34f0171 Capture API prompt when publishing setups (#233)
* feat(sidebar): capture API prompt when publishing setups

* feat(setups): preview and load canonical setup records

* fix(setups): close publish review gaps
2026-06-08 20:28:35 +02:00
malkuthro 85206ab321 feat(setups): add external runner simulator (#231)
* feat(setups): add external runner simulator

* feat(setups): render app-surface simulator form

* docs(setups): capture publish-to-run frontend plan

* docs(setups): preserve sidebar inventory breadcrumbs

* fix(setups): address runner simulator review findings

* fix(setups): validate simulator run payload shape
2026-06-08 08:30:10 +02:00
malkuthro 16c1b42760 Add published setup execution API (#230)
* feat(setups): add published setup execution API

* fix(setups): expose app surface run results

* test(setups): harden execution API failures
2026-06-07 23:08:12 +02:00
malkuthro 83b170bdae feat(setups): add publish contract nodes (#228)
* feat(setups): infer app contract from named fields

* feat(setups): add publish contract nodes

* feat(setups): split publish result contract

* fix(setups): resolve publish output reroutes in preview

* fix(setups): validate app surface on read

* fix(setups): harden app contract validation
2026-06-07 22:02:16 +02:00
malkuthro 7cffe0702e fix(setups): publish simple widget-only workflows (#223)
* fix(setups): publish simple widget-only workflows

* fix(setups): read subgraph and named widget defaults

* fix(setups): allow optional publish descriptions

* fix(setups): reject incomplete widget-only values
2026-06-07 21:50:54 +02:00
malkuthro 3b35e6a195 feat(setups): infer setup surface from workflow groups (#222)
* feat(setups): infer setup surface from workflow groups

* fix(setups): validate inferred setup surfaces on read

* fix(setups): reject boolean setup geometry
2026-06-07 14:45:16 +02:00
malkuthro b4314f3b3d feat(sidebar): preserve workflow groups in saved selections (#220) 2026-06-07 12:41:14 +02:00
malkuthro 98c204b119 feat(setups): convert visual workflows to API prompts (#217)
* feat(setups): convert visual workflows to api prompts

* fix(setups): reject corrupt visual graph links
2026-06-07 11:36:35 +02:00
malkuthro 16023662e6 feat(setups): publish sidebar workflows (#216) 2026-06-07 11:07:52 +02:00
malkuthro 1cf2fd31a2 feat(setups): add published setup catalog API (#214)
* feat(setups): add published setup catalog API

* fix(setups): report registry storage diagnostics
2026-06-06 16:12:21 +02:00
malkuthro 8c8546c17c Add queue-driven loop status controller (#205)
* Add queue-driven loop status node

* Document ComfyUI server API testing

* Harden loop queue controller

* Guard loop controller HTTP requests
2026-06-03 12:34:15 +02:00
malkuthro 7f8c09ad29 feat(sidebar): clean up workflow archives (#201) 2026-06-02 22:25:09 +02:00
8058e17456 fix(snapshot): paired drift-corruption fix — duplicate-install guard + boot-time mismatch (#161, #162) (#186)
* fix(snapshot): prevent drift corruption from duplicate installs + boot-time mismatch (#161, #162)

Closes #162 and #161 as the paired fix the issues describe — both touch
the same data-loss surface and verify against the same dev-sync rig.

installs at import time, log critical, register nothing in the
alphabetical loser; client-side `window` sentinel as belt-and-suspenders;
align .env.example / dev-iteration-loop / sync_to_dev / README at the
Registry-derived `koolook/` path.

compare against live /userdata + picks, set a drift flag on mismatch;
gate periodic + pre-load autosave routing to `_unsaved_autosave/` while
drifted; promote status pill to "drifted" precedence over "saved" with a
recovery tooltip; auto-clear on the next markStateSaved (Save / Load).

26 new tests (14 install guard, 4 sidebar sentinel, 8 boot drift) —
316 total passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snapshot): apply review fixes — canonical fingerprint + early loser exit + drift timestamp

Review-driven follow-ups on PR #186:

1. **Symmetrize file-side fingerprint (MEDIUM).** `detectBootDrift`
   now uses a new `_canonicalStateFingerprint()` that normalizes the
   workflows tree through `normalizeWorkflowsStore` AND deep-sorts keys
   via `sortJsonValue`. Without this, hand-edited / cross-machine /
   pre-v0.3 files would false-positive as drifted on first boot
   (e.g. `archived: "false"` string vs `false` bool, missing
   `directories: {}`, key insertion-order asymmetry from the
   `{...wf, archived, module, tags}` spread). `_computeFingerprint`
   stays unchanged so the localStorage save baseline contract holds
   for existing users. New regression test verifies a shape-only diff
   does NOT trigger drift.

2. **Gate fork imports on `_is_winning_install` (LOW).** Non-winning
   duplicate installs now short-circuit before the heavy torch / PIL /
   diffusers / aiohttp imports, exiting in ~5 LoC instead of paying
   full import cost just to discard the mappings.

3. **Drift timestamp uses `driftDetectedAt`, not stale
   `lastNamedSaveAt` (LOW).** The localStorage save baseline could be
   from a prior session whose live state was already corrupt —
   surfacing it as "Last named save" in the drift tooltip would
   mislead. Replaced with the session-noticed-at timestamp from
   `_bootDriftDiagnostics`.

352 tests passing (316 prior + 26 from PR #186 + 1 new normalize-
symmetry case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install-guard): fail safe on unreadable / non-UTF-8 siblings (#162)

Re-review follow-up. The import-time duplicate-install scan could crash
the entire plugin load (fail-silent — the alphabetical winner vanished
too, not just the loser) on two unguarded paths:

- detect_duplicate_koolook_installs: the per-entry is_dir()/is_file()
  over custom_nodes/ siblings was unguarded, so a non-traversable
  sibling (restrictive perms, broken mount, a special dir) raised
  PermissionError up into __init__.py. No duplicate even required.
  Now caught per-entry and skipped.
- read_pyproject_version: read_text(encoding="utf-8") raises
  UnicodeDecodeError (a ValueError, NOT OSError) on a non-UTF-8 /
  binary pyproject.toml, escaping the `except OSError`. Now degrades
  to "?" via `except (OSError, ValueError)`.
- __init__.py: wrapped the guard call site as a final backstop — any
  unforeseen guard failure now degrades to register-normally rather
  than aborting the import.

Both paths reproduced before the fix; the first surfaced while running
the suite from a /tmp checkout whose parent held a root-owned dir.

3 regression tests added (non-traversable sibling skipped; non-UTF-8
pyproject -> "?"; report survives a non-UTF-8 sibling). Full suite:
452 passed (449 + 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
2026-06-02 18:44:57 +02:00
malkuthro 1d9d40ab70 Add audio transcript timeline node (#189)
* Add audio transcript timeline node

* Hook audio transcripts into Koolook Director

* Add modular timeline audio timing workflow

* fix(frontend): guard Comfy workflow draft cache

* fix(audio): compose transcript timeline prompts

* fix(audio): address transcript review findings

* fix(ci): keep transcript tests lightweight

* fix(timeline): restore image previews from saved paths

* fix(timeline): scrub restored preview blobs
2026-05-30 18:31:41 +02:00
malkuthroandClaude Opus 4.7 4409d3c9d8 feat(ai_pipeline): no_subfolders toggle + path normalisation + tooltips (#156)
* feat(ai_pipeline): no_subfolders toggle + path normalisation + tooltips

Three EasyAIPipeline improvements with a 49-case test suite pinning
the behavior so the user-reported "phantom undefined/ folder" bug
can't regress.

no_subfolders (default off): write directly into base_directory_path
with no shot_name/ai_method/vNNN subfolders appended. Base folder is
still auto-created if missing.

Paste-input hardening: _normalize_base_path strips surrounding
whitespace, one matched pair of quotes ("..." from Explorer's Copy
as path), and trailing path separators. Drive roots (C:\, n:/) are
preserved. Plus a belt-and-suspenders rstrip on the final
output_directory — fixes the symptom where a trailing backslash on
the user's pasted base path caused downstream save nodes to split
on /, stringify the empty tail as "undefined", and write into
.../undefined/... on disk.

Smart filename: empty pieces of shot_name / ai_method / vNNN are
filtered before the underscore join, so blank ai_method yields
RTX-upscale.exr (not RTX-upscale_.exr). Same rule for subfolder
build.

Tooltips on every input explain interactions (e.g. ai_method
becomes both filename segment and subfolder; enable_overwrite only
blocks an existing file, not directory).

JS preview helpers in web/ai_pipeline.js mirror the Python so the
Get output directory / file path buttons stay accurate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai_pipeline): sanitize shot_name/ai_method to prevent base-path escape

Reviewer-flagged blocker in PR #156: os.path.join's "last absolute path
wins" rule let absolute-looking shot_name or ai_method values (typos
like "/oops", or pasted full paths like "C:/Windows/junk") replace
base_directory_path entirely. The pre-#156 code had a partial defense
(.lstrip('/')) that the refactor dropped without replacing.

New _sanitize_segment helper: lstrip leading separators → splitdrive →
lstrip again. Strips leading seps BEFORE splitdrive so multi-slash
input like "///oops" doesn't get swallowed as a UNC prefix on Windows
(empirically observed — the user's intended segment name would be lost).
Applied symmetrically to directory build and filename build. Raw
shot_name kept in the return tuple for downstream label-only consumers.

Also fixes a minor JS preview mismatch at web/ai_pipeline.js: empty
base + no_subfolders=true used to render "/name.exr" while Python
returns "name.exr". Filter empties before joining the file_path.

16 new tests pin both the helper and the end-to-end escape vectors:
- TestSanitizeSegment (7 unit tests, platform-aware drive handling)
- 4 parametrised absolute shot_name escape cases
- 4 parametrised absolute ai_method escape cases
- 1 filename-leak regression

Full pytest: 102 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai_pipeline): flatten embedded separators in shot_name for filename

Maintainer-reported: with no_subfolders=true and an upstream node feeding
shot_name="ComfyUI-working-folder/LTX-Director-2K", the output was
base/ComfyUI-working-folder/LTX-Director-2K.%04d.exr — still creating
subfolders via the filename concat, defeating the toggle entirely.

Root cause: the prior fix sanitized only LEADING separators (path-escape
case). Internal separators in shot_name slipped through and became
subfolders when the filename string with embedded "/" was joined onto
output_directory by the filesystem.

Fix: filenames can't contain "/" or "\\" on any OS, so always flatten
them to "_" in the filename build. Directory build with
no_subfolders=false still uses the separators as subfolder delimiters
(intentional — users organizing into Project/v1/ai_method/vNNN want this).

With no_subfolders=true, shot_name="job/shot" now correctly produces
base/job_shot.ext (flat in base), not base/job/shot.ext.

3 new tests:
- Maintainer's exact case (no_subfolders=true + slash in shot_name)
- Belt-and-suspenders across 5 shot_name/ai_method/toggle combinations
- Verify no_subfolders=false still creates subfolders from slashes
  (preserves intended use case)

Full pytest: 110 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai_pipeline): strip control chars + single-line Output Preview

Maintainer reported the file_path preview rendering as two visual lines
("n:/.../bear/...LTX-Director-2K" + newline + "_0.%04d.exr") and the
output looking like it contained a literal newline. Two contributing
causes:

1. The multi-line Output Preview widget word-wrapped long single-line
   paths at the textarea edge. The data was always one logical string,
   but the visual break made it look broken. Fix: set wrap="off",
   whiteSpace="pre", overflowX="auto" on the input element so long
   paths render on a single horizontal-scrollable line.

2. Upstream nodes (WAS Text Multiline etc.) can legitimately emit
   strings with embedded \n / \r / \t — a stray paragraph break in the
   widget content. Those would silently break filesystem saves and ALSO
   wrap the preview at the actual newline character (not just the
   textarea edge). Fix: new _strip_control_chars helper, applied at the
   top of _normalize_base_path and _sanitize_segment (covers
   base_directory_path, shot_name, ai_method) plus explicitly on
   extension in generate_pipeline. Mirrored in JS for preview parity.

9 new tests: 6 helper unit tests, 3 end-to-end (shot_name w/ newline,
base path w/ newline, extension w/ newline).

Full pytest: 119 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai_pipeline): strict whitespace strip on extension + revert widget

Two issues, one commit:

1. Maintainer reported ComfyUI-HQ-Image-Save bailing with "Filepath
   needs to end in .exr" even though our preview showed a clean
   .%04d.exr suffix. Root cause: HQ-Image-Save validates via
   os.path.splitext(filepath)[1].lower() != ".exr" — a single trailing
   space on the extension widget (easy to acquire from a paste) makes
   splitext return ".exr " with the space, which fails the strict
   equality. Previous control-char strip only handled \r\n\t and
   left regular spaces alone.

   Fix: extension now runs through "".join(extension.split()) which
   removes ALL whitespace (internal + surrounding, including unicode
   whitespace and non-breaking spaces). Mirrored as cleanExtension
   in JS. Also added .strip() to _sanitize_segment so shot_name /
   ai_method with stray surrounding whitespace canonicalise too.

2. Reverted the Output Preview widget from the previous commit's
   single-line wrap=off + horizontal-scroll layout back to the
   original multi-line word-wrap (100px height, default textarea
   behavior). The earlier change traded mouse-drag-selectability for
   single-line display — wrong tradeoff. The path is one logical
   string anyway (cleanLine strips real newlines), so visual wrap is
   just cosmetic.

9 new tests:
- 6 parametrised extension whitespace cases (trailing/leading/internal
  space, tab, non-breaking space) — all verify HQ-Image-Save's
  splitext check passes after sanitization
- 3 parametrised shot_name surrounding-whitespace cases

Full pytest: 128 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai_pipeline): no_subfolders keeps version folder; v### in tooltips

Maintainer-clarified semantics for the no_subfolders toggle: it should
only drop shot_name / ai_method from the directory chain, NOT the
version folder. Previously, no_subfolders=true stripped everything,
which meant versioned outputs all collided in the same flat directory —
defeating versioning whenever the toggle was on.

New behavior matrix for no_subfolders=true:
- disable_versioning=true  → output_directory = base
- disable_versioning=false → output_directory = base/v###

So truly flat output (no subfolders at all) now requires BOTH toggles,
matching VFX convention. Versioned-but-no-name-subfolders is now
expressible with just no_subfolders=true, which is the common case.

Tooltip updates:
- no_subfolders: rewritten to describe new behavior
- base_directory_path: path-shape description updated to
  shot_name/ai_method/[v###]
- ai_method: clarifies subfolder application is gated by no_subfolders
- version, disable_versioning: vNNN → v### (Nuke-style convention
  consistent with the existing .%04d.exr default)

2 new tests:
- no_subfolders=True + disable_versioning=False → base/v###
- no_subfolders=True + disable_versioning=True  → base (truly flat)

Existing tests use the disable_versioning=True default with
no_subfolders=True so they continue to assert base-only — they still
pass under the new logic since version_str="" gets filtered out of the
join.

Full pytest: 130 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai_pipeline): strip sentinel path components

* fix(ai_pipeline): resolve EasyUse get-set preview values

* docs: document ComfyUI preview button debugging

* fix(ai_pipeline): align drive segment sanitization

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:01:43 +02:00
malkuthro 55dab39c9b Trim workflow context menu move list (#153) 2026-05-16 23:22:23 +02:00
malkuthro f74b30603e Release v0.3.2 (#149) 2026-05-16 01:25:39 +02:00
malkuthro 119a1e3e6c docs: refresh release guide screenshots (#148) 2026-05-16 01:05:02 +02:00
malkuthroandClaude Opus 4.7 8e489ad37a feat(snapshot): redesigned Save/Load dialogs + folder picker (#137) (#144)
* feat(snapshot): row-augment max(periodic, pre_load_*) + pytest infra (#137)

Server-side preparation for the snapshot dialogs redesign. The Load
dialog's "newer auto-save available" row flag now reflects the freshest
recovery file regardless of which mechanism wrote it — before this we
only stat'd ``periodic.json``, missing cases where the user's most
recent Load wrote a fresher ``pre_load_*.json`` snapshot inside the
same autosave subfolder.

Extracts ``_latest_autosave_mtime(autosave_dir, named_mtime)`` from the
inline block in ``list_presets`` so the scan is unit-testable. The
helper walks ``periodic.json`` and every ``pre_load_*.json`` once,
returns the max mtime if strictly newer than the named file, else
``None``. Same shape as before, broader source set.

Introduces pytest as a first-class test runner:

  * ``tests/server/test_listing_autosave_mtime.py`` — 5 row-augment
    test cases (periodic-only, pre_load-only, max-of-both, no-newer,
    missing-dir).
  * ``[tool.pytest.ini_options]`` in ``pyproject.toml`` scopes
    discovery to ``tests/`` and uses ``importmode=importlib``.
  * ``[project.optional-dependencies] test = [...]`` defines the
    test extra (pytest + aiohttp, which is a runtime dep of
    ``koolook_routes`` that must be importable even when only a pure
    helper is under test).
  * ``.github/workflows/ci.yml`` gains a ``pytest`` job that installs
    via ``pip install .[test]`` and runs ``pytest -q``.

Guards ``__init__.py``'s relative imports in a ``try / except
ImportError / else`` block. Pytest's package collector walks up from
test files looking for ``__init__.py`` and tries to import the one at
the repo root — but the relative imports inside it only resolve under
ComfyUI's custom-node loader, not under a standalone Python import.
Without the guard, every pytest run aborts during collection with
``attempted relative import with no known parent package``. The
fallback emits a one-line diagnostic and registers empty mappings so
the package directory can still be imported by tooling. ComfyUI's
loader still hits the ``else`` branch and behaviour is unchanged for
real users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(snapshot): folder picker + agent-driven visual harness (#137)

Commit 2 of the snapshot dialogs redesign. Introduces the navigate-
into folder picker from mockup section 6 — the picker that opens when
the user clicks Save to… / Load from… in the redesigned dialogs (and
that the Settings cog will be removed in favour of, once commit 6
lands). Path input is the source of truth: type/paste + Enter
navigates, clicking a folder row drills in, ↑ Up climbs one level.
Files appear greyed below the directories as a "yes, this is the
folder I expected" affordance.

End-visible path overflow: the path input uses ``direction: rtl`` +
``unicode-bidi: plaintext`` so when the path exceeds the input width
the leading ``/Users/…`` clips on the left, never the trailing folder
name. Per mockup section 6.

Visual harness — the path the issue called out as "visual verification
is a hard gate":

  * ``.claude/launch.json`` defines ``design-server`` (``python3 -m
    http.server 8765``). Agent / maintainer can spin this up via
    ``preview_start`` or directly from a terminal.
  * ``docs/designs/_harness/folder-picker.html`` mounts the real
    ``showFolderPicker`` from ``web/sidebar/modals.js`` against a
    canned filesystem, with one trigger button per mockup-illustrated
    state (default / new-folder-input / empty / error). Cache-busts
    module imports so editing modals.js while the harness is open
    picks up fresh code on reload.
  * ``docs/maintainers/visual-harness.md`` documents the workflow,
    when to use the harness vs. dev-sync, and the backtick-in-CSS-
    comments pitfall that broke this commit twice during development.
  * ``CLAUDE.md`` now lists the harness as the preferred agent-driven
    path; dev-sync stays the integration-level gate.

Server: ``/koolook/presets/browse`` gains an opt-in ``?files=1``
parameter that returns child JSON files alongside the directories.
Default response shape unchanged so the legacy Settings dialog
keeps working. ``_list_child_files`` mirrors ``_list_child_dirs``'s
swallow-OSError-per-entry policy so a broken symlink mid-listing
doesn't sink the whole response.

Tests: six new pytest cases in
``tests/server/test_browse_list_children.py`` covering directory
sorting, autosave-subfolder hiding, JSON-only filter, case-insensitive
extension match, and the empty-result paths. ``pytest`` runs all 11
tests cleanly under the existing CI job from commit 1.

What's still verified via dev-sync (not via the harness):
  * Real Comfy theme tokens (the harness fakes ``--comfy-menu-bg``
    etc. with dark-theme defaults).
  * End-to-end Save → Load round-trip across the new library path.
  * Picker interaction with ComfyUI's keyboard manager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snapshot): modal headers stay title-only; route gloss to tooltip (#137)

Follow-up to the folder picker commit. Maintainer flagged that the
"Where snapshots are saved and loaded from." subtitle below the title
was extra information the user doesn't need at first glance — if the
title isn't self-explanatory, fix the title, don't pad it with a
sentence. The mockup at docs/designs/snapshot-dialogs.html section 6
shows a title-only header for exactly this reason.

Locks the rule in as a project convention so every dialog box behaves
the same way going forward:

  * docs/maintainers/conventions.md — new "Modal headers" section
    spells out the rule, what counts as a "descriptive subtitle"
    (remove) vs. a "functional info row" (keep), and how to plumb
    explanatory copy via a hover tooltip on the title instead.
  * web/sidebar/modals.js — makeModalShell now accepts ``titleTooltip``,
    applied as the ``title`` attribute on the modal title element so
    the gloss is mouseover-discoverable but never visible by default.
    showFolderPicker drops its ``subtitle`` param (it was added one
    commit ago, no other callers) in favour of ``titleTooltip``.
  * docs/designs/_harness/folder-picker.html — harness re-points its
    explanatory text to ``titleTooltip``. Verified in the harness: the
    visible header is title-only, the tooltip surfaces on hover.

Applies prospectively to the Save / Load dialogs (commits 3 and 4) —
they will also be title-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(snapshot): redesigned Save dialog with inline library row + Save to... + 4-state button (#137)

Commit 3 of the snapshot dialogs redesign. The Save dialog is now a
single modal (was three: three-way overwrite / input prompt / confirm)
that matches mockup section 2:

  * Inline library-path info row at the top of the body — "Saved to"
    label on the left, "Open folder ↗" link on the right, full path
    below. Pure information; every action lives in the bottom command
    bar.
  * Bottom command bar with split layout: Save to... on the left,
    Cancel + Save as new... + Save on the right (spacer in between).
  * Save to... opens the folder picker from commit 2. On Use this
    folder, the dialog calls saveSettings() to persist the new library
    path, refreshes its in-place library row, and flips the primary
    button into Dirty state.
  * The primary Save button cycles through the four states from
    mockup section 5:
      - Default ("Save") — clean state, primary blue.
      - Dirty ("Save to new folder") — folder differs from the one the
        dialog opened with; the label spells out the consequence.
      - In progress ("Saving...") — disabled, dimmed primary.
      - Done ("Saved") — disabled, subtle green, brief pause before
        the dialog closes.
  * Save as new... still routes through the existing showInputModal
    plus presetExists guard, but the prompt no longer carries a
    library-path subtitle — the parent dialog already shows the
    target. Convention from the previous commit applied.

tree.js now passes the new capability functions (saveSettings,
browseDirectories, createBrowseDirectory, revealPresetFolder) into
showSaveSnapshotDialog so the dialog can invoke the folder picker
end-to-end inside the Save flow.

Visual harness: docs/designs/_harness/save-dialog.html exercises all
five mockup states (default tracked, untracked Save-as-new, dirty,
in-progress, done) so the maintainer (or future agent) can re-verify
without dev-syncing. Verified all states render correctly in the
preview server.

Static guard against the recurring backtick-in-CSS-comments trap:
tests/sidebar/test_css_template_literal.py asserts that the CSS
template literal in web/sidebar/constants.js contains exactly two
backticks (open + close) and nothing in between. The trap bit this
work three times (commits 2 and 3) — a literal backtick inside a
comment closes the template early and silently bricks the entire
sidebar. The test runs under the same pytest job introduced in
commit 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(snapshot): redesigned Load dialog with scoped recovery + inline delete (#137)

Commit 4 of the snapshot dialogs redesign. The Load dialog is now a
single modal matching mockup section 3:

  * Inline library row at the top, same shape as the Save dialog —
    "Loaded from" label + Open folder ↗ link + leaf folder name +
    full path. No 📂 emoji button (the link supersedes it).
  * Preset rows: click to load. The "Replace current state?" confirm
    modal is gone — clicking Load is the intent, the dialog title
    already says Load snapshot, no double-confirm.
  * Scoped recovery: when a preset has a newer autosave (per the
    server-side row-augment from commit 1), the first click DOES NOT
    load. It expands exactly ONE recovery row directly beneath the
    parent, showing the freshest entry (newest of periodic.json or
    pre_load_*.json) with its kind badge + meta line. The user then
    chooses: re-click the named row to load the named version, OR
    click the recovery row to restore the auto-save. Both options
    are surfaced inline; the user picks by clicking the one they
    want.
  * Inline delete confirm: × outlines the target row red, the bottom
    Close button transforms to 'Yes — delete "<name>"' (danger
    styling). A second click commits; Escape cancels and reverts.
    No second modal.
  * Bottom command bar matches mockup section 3 split layout:
    [Load from…] on the left, [Close] on the right. Load from…
    opens the folder picker from commit 2; on Use this folder it
    persists via saveSettings() and refreshes the listing.

The pre-#137 YES/NO choice modal (``promptApplyChoice``) and the
collapsible Recovery auto-saves <details> section are both removed —
the scoped-recovery inline row + the click flow above subsume their
job. (Commit 5 in the issue plan was specifically for dropping the
YES/NO modal, which happens here as a natural consequence of the
routing change; commit 5 will be a small follow-up if needed.)

Behavior preserved from the pre-#137 implementation:
  * doNamedLoad — pre-load auto-save backup chain, partial-failure
    tracker handling, identical toast messages.
  * doAutosaveRestore — same backup chain, same markStateAutosaved()
    semantics, same restored-name derivation from subdir.

Tree.js plumbs the three new caps (saveSettings, browseDirectories,
createBrowseDirectory) into showLoadSnapshotDialog so the dialog can
invoke the folder picker end-to-end.

Visual harness: docs/designs/_harness/load-dialog.html exercises
default, empty, scoped-recovery, and pending-delete states. Verified
all four render correctly in the preview server.

Net diff: -56 lines in the sidebar source (the redesign deletes more
than it adds because the YES/NO modal + collapsible recovery
disclosure + confirm modal for plain loads are all gone).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(designs): reconcile snapshot redesign sketch with shipped behaviour (#137)

Commit 5 of the snapshot dialogs redesign. The code-level removal of
the autosave-newer YES/NO modal happened in commit 4 as a natural
consequence of the scoped-recovery inline row taking over its job;
this commit reconciles the design docs so they describe what was
actually built.

docs/designs/snapshot-dialogs-redesign.md:
  * "What changes" item 4 reworded — the YES/NO modal is dropped;
    the affordance is now an inline scoped recovery row that opens
    directly beneath the clicked preset.
  * "What changes" gains items 2, 3, 5 covering the Save/Load dialog
    redesigns and the inline delete confirm that landed in commits 3
    and 4.
  * Mermaid flow rewritten to reflect the new routing (preset click
    → scoped recovery expand → user picks named or recovery row),
    matching commit 4's onPresetClick implementation.

docs/designs/snapshot-dialogs.html "Locked-in decisions":
  * Updated the autosave-modal item from "modal head reads X" to
    "modal — dropped" and explains the inline scoped recovery row
    replacement.
  * Added the title-only modal header convention (commit 2 follow-up)
    pointing to docs/maintainers/conventions.md.
  * Added a pointer to the visual harness workflow added in commit 2.

No code changes — the YES/NO modal was already gone after commit 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(snapshot): remove Settings cog + showSnapshotSettingsDialog + CHANGELOG (#137)

Final commit of the snapshot dialogs redesign. The Settings cog disappears
from the Snapshot toolbar — the snapshot row is now status indicator
| Load | Quick Save | Save, matching mockup section 1. Library-path
management now lives inside the Save dialog ("Save to…") and Load
dialog ("Load from…"), both of which open the folder picker introduced
in commit 2 and persist via the existing `saveSettings` route.

Removals:
  * `showSnapshotSettingsDialog` (~325 lines) — the dialog body, its
    inline directory browser, and the "save folder location" state
    machine. The same browse functionality lives in `showFolderPicker`
    now; the same persistence happens via `saveSettings()` called from
    inside Save / Load.
  * `showThreeWayDialog` (~45 lines) — only consumer was the pre-#137
    Save flow (overwrite / Save as new / Cancel), which the redesigned
    Save dialog absorbed into its primary action bar.
  * `renderLibraryLocation` (~17 lines) — replaced by inline rendering
    in the Save and Load dialogs' library rows.
  * Settings cog toolbar button + `openSettingsDialog` wiring in
    tree.js (~12 lines).
  * Orphan `.koolook-browse-*`, `.koolook-path-input-row`,
    `.koolook-load-library-row*`, `.koolook-settings-save-note` CSS
    rules in constants.js (~16 lines) — only consumed by the removed
    dialog. `.koolook-settings-folder-name` and `.koolook-settings-
    folder-path` stay; the new Save / Load library rows reuse them
    verbatim (git-blame continuity for the visual style).

Adds:
  * CHANGELOG.md [Unreleased] entry summarising the whole #137
    feature — picker, redesigned Save/Load dialogs, scoped recovery,
    inline delete, removed Settings cog, pytest infrastructure.

Net diff for this commit: -377 lines.

Cumulative PR diff (commits 1–6 + harness/convention follow-ups):
all six issue ACs satisfied. The maintainer should dev-sync once
before merging to confirm real-Comfy theme tokens render correctly
(harness fakes the dark-theme defaults) and end-to-end Save → Load
round-trips against the live preset folder behave.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): install pytest + aiohttp directly, not via pip install . (#137)

The Pytest job introduced in commit 1 of this PR was failing on CI
because ``pip install .[test]`` tries to build a wheel of the entire
project. setuptools's flat-layout discovery then trips on the
non-Python ``web/`` and ``forks/`` directories at the repo root and
errors out with "Multiple top-level packages discovered in a flat-
layout: ['web', 'forks']".

The project is a ComfyUI custom node loaded directly from its source
directory, not a pip-installable package. There's no wheel to build.
Install the test deps explicitly instead — the ``[project.optional-
dependencies] test`` block in pyproject.toml is the spec, CI mirrors
it by hand.

Worked locally because the venv install path went straight to
``pip install pytest aiohttp`` and never touched the project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snapshot): untracked dirty state + Load dialog Escape listener leak (#137)

Two findings from the external review on PR #144:

**Blocker — Save dialog's primary action with no tracked preset + folder
changed via Save to...**

``updatePrimaryLabel`` was entering the "dirty" state whenever the
library path diverged from the original, regardless of whether a
tracked preset existed. The primary click handler then routed "dirty"
to ``doOverwrite(current)`` — and with ``current === null``,
``sanitizeName(null)`` would throw / silently coerce to an empty
string instead of asking the user for a name.

Fix: ``isDirty()`` only matters when there is something to overwrite.
With no tracked preset, the primary stays in ``default-new``
regardless of folder divergence. The Save to... button still updates
the library row, so the user has feedback the destination changed; the
eventual primary click then routes to the Save-as-new name prompt and
writes into the new folder cleanly.

Verified in the visual harness: opening Save as untracked, switching
the library to ``/Volumes/Shared/different-library`` via the picker,
then inspecting the primary button:

  * Label stays "Save" (was "Save to new folder" pre-fix)
  * Tooltip "Choose a name and write the snapshot." (was "Write into
    /Volumes/Shared/different-library.")
  * Library row still reflects the new path (saveSettings DID run)

**Major — Load dialog leaked a document keydown listener per session**

The inline-delete Escape-to-cancel handler was registered with
``document.addEventListener`` but never removed. Each open/close cycle
of the Load dialog accumulated another stale handler; long sessions
ended up firing the cancelDelete check on every Escape press across
every dialog (no-op because ``pendingDelete`` was null in closed
dialogs, but the listener was still alive and called).

Fix: capture the handler by name, attach on open, tear down on close
by wrapping ``overlay.remove`` (mirrors the pattern the modal shell
already uses at ``modals.js:52-66`` for its own Escape handler).

Verified by instrumenting ``document.{add,remove}EventListener`` and
opening + closing the Load dialog five times in a row: every keydown
listener added during the cycle was removed before the next cycle
opened. No accumulation.

CI gates: ruff, compileall, bandit (via preflight equivalent), pytest
12/12, preflight 4/4 — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:00:34 +02:00
malkuthroandClaude Opus 4.7 e87d111521 docs(designs): snapshot dialogs redesign mockup + Mermaid flow (WIP) (#136)
* docs(designs): snapshot dialogs redesign mockup + Mermaid flow (WIP)

Design exploration for consolidating the Snapshot Settings cog into the
Save and Load dialogs, plus the related autosave-newer fix. Discussion
artifacts only — no code changes.

- docs/designs/snapshot-dialogs.html: side-by-side mockups across six
  sections (toolbar, Save dialog, Load dialog with default / Recovery
  expanded / inline delete-confirm states, autosave-newer modal,
  button-state pattern, folder-browse picker)
- docs/designs/snapshot-dialogs-redesign.md: scope sketch + Mermaid flow
- docs/designs/README.md: register the two new design files in the index

Single-library design stays (documented intent per
docs/maintainers/curated-sidebar.md). Redesign only consolidates surface;
implementation lands in a separate PR after layout details are locked in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(designs): refine autosave-newer modal + single-row recovery groups

Two scoped tweaks to the snapshot dialogs mockup:

- Section 4 (autosave-newer): drop the "Replace current state?" question
  since the user already chose Load; promote "Auto-save is newer than
  the saved version" to the only head message. Render the two options
  as standard preset-rows (filename + picks/workflows/date meta) so the
  layout mirrors the Load dialog list. Buttons collapse to NO /
  YES — load latest on one row.
- Section 3 (recovery expanded): show only the newest row per
  <preset>_autosave/ group — whichever of Pre-load or Periodic is most
  recent — instead of listing both kinds. Annotation also notes the
  Recovery section auto-expands when a preset row is clicked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(designs): lock recovery behavior — hidden by default, one row per clicked preset

When the user clicks a preset row (Koolook_v03, starter, etc.) the recovery
section opens and reveals only that preset's <preset>_autosave/ folder,
with a single row inside — whichever of Pre-load or Periodic is newest
(the system picks). _unsaved_autosave/ no longer renders inside this
preset-scoped view since it isn't tied to the clicked preset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(designs): polish section 6 + recovery row layout

Folder-browse picker (section 6):
- Switch to standard navigate-into model — clicking a folder drills in,
  body lists that folder's contents (subfolders clickable, files greyed-
  out for context). Path input shows where you are; "Use this folder"
  commits the current path. Drops the sibling-list + selected-tag model.
- Single-line toolbar: ↑ Up next to the path input; secondary parent-
  path strip removed.
- Path input keeps the END visible when overflowing (direction:rtl +
  text-align:left), so a long path shows the meaningful trailing folder
  instead of /Users/… — and example path is now long enough to
  demonstrate it.
- Header restyled to match other modal heads (15px title, dark bg,
  bigger padding); descriptive subtitle removed.
- Bottom bar: New folder… stays left; Cancel + Use this folder push
  right (.picker-foot .spacer rule, since the existing flex-spacer was
  scoped to .modal-actions).
- Redundant "SELECTED FOLDER" stripe removed — selected-row blue accent
  is sufficient confirmation; folder list gains the freed vertical room.
- Example folder rename _unsaved_autosave -> default_autosave (label
  only — on-disk constant unchanged; rename is an open suggestion to
  drop the self-contradicting "unsaved autosave" term).

Recovery row (section 3):
- Layout: badge on top, single combined meta line below in order
  "<picks> · <workflows> · <date>" — matches preset-row shape so both
  rows read the same way.
- Padding bumped to 10px 12px so recovery-row and preset-row are the
  same box size; badge nudged 1px up + meta nudged 1px down for
  breathing room.
- Recovery section now scopes to the clicked preset only: orphan
  _unsaved_autosave/ group removed from the "Koolook_v03 clicked" view,
  since when a preset row is clicked the dropdown should only show that
  preset's auto-saves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: require visual verification before requesting design-driven review

When implementing a UI change that has a corresponding mockup in
docs/designs/ (e.g. snapshot-dialogs.html, sidebar-icon-proposals.html),
the design file is the spec. Before the work is brought to the
maintainer for review, the implementing agent must:

  1. Render the implementation in a real browser — dev-sync for runtime
     sidebar work, python3 -m http.server (or the Launch preview panel)
     for standalone HTML mockups.
  2. Screenshot the implemented UI at every state the mockup shows.
  3. Diff each screenshot against the mockup. Match it or document the
     deviation with a rationale.
  4. Only then request review.

The maintainer should never be the first set of eyes on rendered
output. Type checks and tests verify code correctness, not visual
fidelity to the design — if a mockup exists for the change, visual
verification is a hard gate, not optional polish.

Rule lives in three places, each hit from a different entry point:
- CLAUDE.md (loaded at every session start; canonical)
- docs/designs/README.md (discovered when opening the designs folder)
- docs/maintainers/dev-iteration-loop.md (step 4 in the iteration loop)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skills(tdd): add project-specific TDD skill modeled on ForgeFlow's

When implementation work on docs/designs/ mockups begins, /tdd on this
repo should pick up a project-level skill that knows ComfyUI-Koolook's
actual machinery — not just generic TDD principles.

Two files:

- SKILL.md — keeps the global TDD philosophy (vertical slices,
  behaviour-not-implementation, refactor only after GREEN) and adds a
  project-specific tail covering:
  • code surface: where Python nodes vs sidebar JS vs design specs live
  • test surface: what CI runs today (ruff, compileall, bandit,
    preflight_release.py) and explicit guidance on introducing pytest /
    vitest "only when there's a behaviour that genuinely needs unit
    testing" — same PR as the first test, never speculative
  • the dev-sync iteration loop for runtime sidebar work
  • high-value test targets (node registration, route validation, VAE
    dispatch, snapshot row augmentation)
  • drift patterns specific to this codebase (vendored upstream into
    MAIN, renamed namespaced node IDs in saved workflows, hardcoded
    user paths, dev-sync triggered from an automated flow, …)
  • final scope-alignment check routing (clean → proceed; drift → show
    report, never revert)

- visual-verification.md — the secondary procedure for cycles whose
  assertion is CSS / layout / structural. Procedure split for the two
  shapes of work in this repo:
  • runtime sidebar (web/sidebar/*.js): dev-sync → restart ComfyUI →
    screenshot → diff against docs/designs/<feature>.html
  • standalone HTML mockups (docs/designs/*.html): Launch preview panel
    or python3 -m http.server (no new package installs per
    supply-chain rules)
  Plus a structured definition of what "diff against the mockup" means
  (spatial, hierarchy, states, chrome, text — not pixel-equality), so
  it isn't a vague gate.

Generic companions (deep-modules, mocking, refactoring, interface-
design, scope-check, tests) intentionally not copied — agents follow
the global ~/.claude/skills/tdd/ pointers in SKILL.md for those. Avoids
sync-drift on principle docs and keeps this skill focused on what's
genuinely project-specific.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:43:55 +02:00
malkuthro e5e07f6c50 feat(sidebar): preserve subgraph definitions in modules (#103) 2026-05-06 22:13:34 +02:00
malkuthro 6f1025f66b fix(sidebar): preserve module links on insert (#102) 2026-05-06 19:08:57 +02:00
malkuthroandClaude Opus 4.7 413b153702 docs(maintainers): refresh stale references + add conventions.md
Audit triggered while debugging the v0.2.0 release surfaced drift in
the maintainer docs vs current code: dead curated_defaults.json
references (file replaced by starter_preset.json in PR #72), the
v1_0_1 fork example (removed in v0.1.5), and a "tooling we don't have
yet" entry that's now covered by tools/preflight_release.py. Adds a
new conventions.md for cross-cutting rules — currently the
Mermaid-for-diagrams convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:38:07 +02:00
malkuthroandClaude Opus 4.7 cbf2cb4d09 chore(ci): drop push-to-main publish trigger — dispatch-only (#91)
The push trigger fired on every release-PR squash-merge to main,
attempting to publish a version that workflow_dispatch on the prep
branch had already published — producing a 400 "node version already
exists" failure and a cosmetic red badge on the merge commit. With
the dispatch-from-prep-branch flow now standard (per CLAUDE.md and
releasing.md), the push trigger is redundant.

Updates docs/maintainers/releasing.md to match: drops the stale
"trade-off" callout (whose premise was the auto-publish on merge),
replaces the two anti-patterns whose rationale was the auto-trigger
with a single "don't forget to dispatch" reminder.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:34:41 +02:00
malkuthroandCursor ed6f3dad02 feat(sidebar): Modules — splice a saved cluster into the live canvas
Tag any saved workflow with the literal tag `module` and the Kforge
Labs sidebar starts treating it as a reusable building block: a green
`pi pi-plus-circle` icon replaces the file glyph, left-click
**inserts** the cluster at the viewport center (with internal links
re-created and the new nodes left selected), and a distinct hover
tooltip surfaces the changed semantics. Designed for the
`EXR-output stack` / `depth-only render` / `Wan 2.2 prompt
module` pattern: small clusters the maintainer wants to drop into
many workflows without rebuilding them by hand.

What changed:

1. New non-destructive primitive `insertWorkflowOntoCanvas` in
   `web/sidebar/canvas_io.js`. Pre-flights every referenced node type
   against `LiteGraph.registered_node_types` and aborts cleanly with a
   toast naming the missing types — a partial insert with stub nodes
   is worse than no insert. Then deep-clones the saved graph, calls
   `node.configure(...)` per node with `node.id = -1` so
   `app.graph.add()` allocates fresh ids that can never collide with
   the live canvas, and finally walks the saved `links[]` to
   recreate internal connections via `originNode.connect(...)` (which
   auto-allocates fresh link ids — no manual link remap needed). Bbox
   placement reuses the existing CSS-pixel correction so the cluster
   lands at the visible viewport center on HiDPI displays.

2. Right-click on any workflow row now offers `Insert into canvas`
   alongside the existing `Load` — Load remains available for every
   row regardless of tag, so a module can still be loaded as a full
   session if the user wants.

3. Convention-driven module classification in
   `web/sidebar/constants.js` (`MODULE_TAG = module`). Picked a
   tag instead of a new `isModule` schema field so existing saved
   entries can be promoted to modules with one right-click → Tags add,
   no migration needed, and the existing Tags section groups them for
   free. Tree rendering looks up the tag at row-render time
   (`isModuleWorkflow` helper) and switches both the leaf icon
   (green `pi pi-plus-circle`) and the left-click handler (Insert
   instead of Load). Active rows in the Workflows section AND every
   row under the Tags section honor the flip; archived rows stay on
   Load (they're old versions, `review` not `splice`).

4. `showSaveWorkflowModal` gains a `Save as module` checkbox. The
   selection-save toolbar button passes `defaultModule: true` (the
   typical `module` candidate is a small cluster the user just
   selected); the whole-canvas save passes `defaultModule: false`.
   The tag is added in the SAME `persistMutation` as the entry write
   (folded into the `mutate` callback in `saveAndToast`), so a
   commit failure rolls back BOTH the save and the tag together — no
   `module`-but-untagged ghosts.

Docs: new `Modules` section in
`docs/maintainers/workflows-sidebar.md` covering the convention, the
checkbox flow, per-surface behavior, and how to promote an existing
saved workflow to a module without re-saving.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 21:32:06 +02:00
malkuthroandClaude Opus 4.7 a6a31c8086 feat(sidebar): Tools row + starter preset replaces curated_defaults (#72)
* fix(sidebar): merge preset HEAD route into GET handler

PR #68 shipped koolook_routes.py with @routes.get + @routes.head on the
same path. aiohttp's add_get auto-registers HEAD by default, so the
explicit @routes.head collided with the auto-synthesized HEAD and
crashed ComfyUI startup with "Added route will never be executed,
method HEAD is already registered".

Trying allow_head=False on the GET to opt out triggered a second crash:
ComfyUI's server.py mirrors every PromptServer route into a parallel
/api/... table via api_routes.route(method, path)(handler, **kwargs),
and RouteTableDef.route's inner closure rejects unknown kwargs with
TypeError. So no kwarg-based opt-out is viable.

Drop the dedicated HEAD handler. Aiohttp's auto-HEAD-from-GET delivers
HEAD probes to get_preset; we branch on request.method to skip the
file read for HEAD — preserving the cheap-existence optimization the
client's presetExists relies on for multi-MB JSON over Dropbox / iCloud
/ NFS-mounted libraries. CHANGELOG [Unreleased] updated to match.

This bug never reached users — koolook_routes.py was added in #68
(post-v0.2.0) and was never published to Comfy Registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sidebar): Tools row + starter preset replaces curated_defaults

Two coordinated UX changes for fresh-install distribution + admin
ergonomics — designed together and shipped as one unit.

1. New "Tools" row sits between the Snapshot row and the search field.
   Moves the Export and "Install missing for picks" buttons up out of
   the Nodes row (admin/advanced operations don't belong in the
   everyday + Add toolbar). Adds a new "Drop placeholders onto canvas"
   button — the security_level=normal escape hatch for installing
   missing packs: instantiates one placeholder per missing pack on a
   fresh canvas tab so ComfyUI/Manager's standard "Install Missing
   Custom Nodes" detection takes over via Manager's UI flow (which
   uses different gates than our programmatic /customnode/install/
   git_url call). Nodes row keeps only the everyday + button.

2. curated_defaults.json retired in favor of starter_preset.json.
   Fresh installs no longer get picks seeded directly into localStorage
   on first load. Instead the bundled web/starter_preset.json (full
   snapshot format — picks + workflows + tags + archive) is copied
   into the user's snapshot library directory as starter.json. The
   user opens Snapshot -> Load -> starter to apply in one click.
   Unifies fresh-install distribution with the per-user snapshot
   library (#68); maintainer flow becomes "build state in ComfyUI ->
   click the (down-arrow) Tools-row button -> paste over
   web/starter_preset.json -> commit." Existing users with non-empty
   picks are explicitly skipped by the new seeder, so their state is
   untouched.

Removed: seedDefaultsIfNeeded, exportPicks, web/curated_defaults.json,
SEEDED_KEY, DEFAULTS_URL.

Added: seedStarterPresetIfNeeded, exportStarterPreset (snapshot.js),
STARTER_SEEDED_KEY, STARTER_URL, STARTER_PRESET_FILENAME,
discoverMissingPacks (installer.js), dropPlaceholdersForPacks
(canvas_io.js), web/starter_preset.json.

Docs: CLAUDE.md "Curated Nodes sidebar — distributed defaults" ->
"Kforge Labs sidebar — starter preset distribution".
docs/maintainers/curated-sidebar.md rewritten.
CHANGELOG.md [Unreleased] updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:18:15 +02:00
malkuthroandClaude Opus 4.7 0d4c81295b chore(sync): --init flag for first-run dev-sync setup + Mac path docs (#61)
Background
----------
Setting up `scripts/sync_to_dev.py` on a fresh machine required two
pre-steps the script didn't help with: (1) editing `.env` to point at
a path that didn't exist yet, then (2) `mkdir -p` on that path before
the first sync would work. The script errored out cleanly with
"target does not exist" but didn't tell the user the obvious next
move was to create it. This was particularly silent on Mac where the
ComfyUI install path differs from the Windows examples in `.env.example`.

Changes
-------
- **`--init` flag** on `scripts/sync_to_dev.py`. When the target
  directory doesn't exist and `--init` is passed, the script creates
  it and proceeds with the sync. Without `--init`, it errors out with
  the same "first time on this machine?" hint pointing at the new flag.
- **Safety guards** on `--init`. Refuses (exit 3) if:
  - the parent directory doesn't exist, OR
  - the parent doesn't look like a ComfyUI `custom_nodes/` folder
    (heuristic: parent is named `custom_nodes` OR sits inside a
    directory named `ComfyUI`).
  This prevents a typo in `KOLOOK_COMFYUI_DEV_PATH` from quietly
  creating a `ComfyUI-Koolook/` somewhere unexpected on disk. The
  refusal message includes a manual escape hatch (`mkdir -p` + retry
  without `--init`) for the case where the parent really IS the
  ComfyUI install but doesn't match the heuristic.
- **Drop stale `k_easy_version.py`** from `RUNTIME_PATHS`. The file
  was deleted in v0.1.10 (PR #41); the script's `if not src.exists()`
  guard meant it was a no-op every run, but the entry was misleading.
- **`.env.example`** gains explicit macOS / Linux / Windows path
  examples for `KOLOOK_COMFYUI_DEV_PATH` and a 3-step first-run
  recipe (cp .env.example → edit → `--init`).
- **`docs/maintainers/dev-iteration-loop.md`** gains a "First-time
  setup on a new machine" section with the per-OS path table and the
  same 3-step recipe.

Verification
------------
- `python scripts/sync_to_dev.py --dry-run` against a missing target
  → exit 2, hint at `--init`.
- `python scripts/sync_to_dev.py --init` against a real ComfyUI
  `custom_nodes/` parent → creates the leaf, copies 9 entries, exits 0.
- `python scripts/sync_to_dev.py` (no flag) re-run after `--init` →
  re-syncs cleanly, idempotent.
- `python scripts/sync_to_dev.py --init` against `/tmp/not-custom-nodes`
  → exit 3, "parent does not exist".
- `python scripts/sync_to_dev.py --init` against an existing
  `/tmp/wrong-parent` (not custom_nodes) → exit 3, "doesn't look like
  a ComfyUI custom_nodes/ folder", manual escape hatch in the message.
- `python -c "import ast; ast.parse(open('scripts/sync_to_dev.py').read())"`
  → syntax OK.
- `python tools/preflight_release.py --skip manager-meta -v` → PASS.

No CHANGELOG entry — dev-tooling under `scripts/` doesn't ship to
users of the published package. Avoiding noise on the [Unreleased]
section that PRs #59 / #60 also touch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:58:24 +02:00
malkuthroandClaude Opus 4.7 c024a94e38 feat(sidebar): recursive subdirectories under workflows + rebrand to Kforge Labs (#54)
* feat(sidebar): recursive subdirectories under workflows + rebrand to Kforge Labs

## Recursive subdirectories

Workflow directories can now host nested subdirectories at arbitrary
depth. Each nested directory behaves like a top-level one: it can hold
workflows, its own Archive subfolder, and further subdirectories.

- **Create**: right-click a directory → "Create subdirectory…"
- **Save into**: pick the path from the save modal's flat directory
  dropdown (`UP-scale / Type-A`)
- **Move across**: right-click a workflow → "→ <other path>" submenu
  lists every other path in the tree
- **Reserved name**: subdirectory names cannot be "Archive" (case-
  insensitive) at non-root levels — collides with the synthetic Archive
  folder rendering. Top-level "Archive" still allowed (root has no
  archived workflows of its own).

### Schema (recursive, back-compat)

  workflowsCache = { directories: { [name]: DirNode } }
  DirNode = { workflows: { [wfName]: WorkflowEntry }, directories: { [name]: DirNode } }

Pre-v0.3 stores without the nested `directories` field still load fine —
`normalizeWorkflowsStore` treats a missing `directories` as `{}` and
adds the empty field during normalization. No data loss on upgrade.

### Path-based store API

Every mutator/lookup now takes a `path: string[]`:

  addDirectory(parentPath, name)
  renameDirectory(parentPath, old, new)
  deleteDirectory(parentPath, name)
  saveWorkflowEntry(path, wfName, graph)
  archiveWorkflow(path, wfName) / unarchiveWorkflow(...)
  renameWorkflow(path, old, new) / deleteWorkflow(path, wfName)
  moveWorkflow(srcPath, wfName, dstPath)
  getWorkflowGraph(path, wfName)

New helpers: `listDirectoryNames(parentPath = [])`,
`listAllDirectoryPaths()` (DFS), `pathsEqual(a, b)`. `dirOf([])` returns
a synthetic root wrapper so callers don't need a special-case for root.

### UI changes

- **Save modal**: directory dropdown is now a flat list of every path in
  the tree (`JSON.stringify(path)` values, `path.join(" / ")` labels).
  "+ New directory…" creates a top-level dir; subdirs go via right-click.
- **Directory header counts**: now show recursive descendant total
  (active + archived + all subtree). A parent with empty direct
  workflows but populated subdirs no longer shows "0".
- **Save flow**: `saveAndToast` pins every ancestor (`workflows`,
  `workflows/seg1`, `workflows/seg1/seg2`, …) so a save into a deeply
  nested path opens the whole chain.
- **Render order per directory**: subdirs first → Archive folder →
  active workflows last (so the freshest version sits closest to the
  bottom).

## Rebrand: "Curated Nodes" → "Kforge Labs"

- `TAB_TITLE`: "Curated Nodes" → "Kforge Labs"
- `TAB_TOOLTIP`: "Curated selection of ComfyUI nodes" →
  "Kforge Labs — curated nodes & workflows"
- Canvas right-click menu item: "Add to Curated Sidebar" → "Add to Kforge Labs"
- Empty-state hint in the curated-nodes section follows the new label

`TAB_ID` (`koolook.curatedNodes`) and the `app.registerExtension` name
(`koolook.curated_sidebar`) are deliberately unchanged so existing
per-user tab state (pinning, tab order, idempotency sentinel) is
preserved.

## Files

- web/sidebar/constants.js (rebrand)
- web/sidebar/canvas_menu.js (rebrand)
- web/sidebar/canvas_io.js (loadWorkflowOntoCanvas takes path)
- web/sidebar/workflows_store.js (recursive schema + path-based API)
- web/sidebar/modals.js (save modal flat-path picker)
- web/sidebar/tree.js (gather recursion + recursive renderer + path-based context menus)
- docs/maintainers/workflows-sidebar.md (subdirectories section + recursive JSON shape)
- CHANGELOG.md ([Unreleased] entry)

Encapsulation invariants preserved: `workflowsCache` only in
`workflows_store.js`; `pathStates` only in `tree.js`.

Closes the maintainer's outstanding feature request for subfolders under
workflow directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): show empty directories in tree when no search is active

User-reported: right-click → Create subdirectory → name → Submit
appeared to do nothing. The directory was actually created in the cache
(and showed up correctly in the save modal's path picker), but
gatherDirsAt was dropping any directory with zero matched workflows AND
zero matched descendants — including freshly-created empty subdirs.

Fix: scope the empty-drop check to "search active" only. With no search,
every directory in the cache renders. With a search, the filter still
collapses paths that contributed no hits, so search results stay clean.

Verified via Node dry-test (.tmp_subdir_test.js, since deleted):
- Pre-fix: addDirectory(['UP-scale'], 'Test') succeeds; gather('') drops Test
- Post-fix: gather('') includes Test; gather('hires') still drops empties

Also confirmed there is NO case-sensitivity restriction — 'Test' and
'test' coexist as separate directories. The user's earlier observation
that "only the lowercase one shows" was the empty-dir bug masquerading
as a case issue (whichever directory had been saved into earlier was
visible; the other was hidden).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): cascading directory picker in save modal (multi-step)

User feedback: the flat path list was unusable with deep nesting — every
existing path appeared in one giant dropdown, you couldn't pick a parent
and then narrow to a child. Replaced with cascading selects.

Behavior:
- Top-level select shows existing top-level dirs + "+ New directory…"
- Picking a top-level with children reveals a level-2 select. The first
  option is "(save in <path>)" which stops drilling at this level.
  Following options are the children.
- Picking a level-2 child with grandchildren reveals a level-3 select
  with the same shape, etc., to arbitrary depth.
- "(save here)" at any level commits the save at that depth.
- Picking a different top-level / parent collapses every cascade level
  below it and rebuilds.
- `defaultDir` (a path array) auto-drills the cascade to that path on
  open, with onCascadeChange recursing through pre-selected levels.

The save modal's Base-on-existing / Action / Workflow-name sections
re-derive from `getSelectedPath()` whenever any cascade level changes,
so they reflect the current target dir.

Verified via Node dry-test (.tmp_cascade_test.js, since deleted) over 6
scenarios: fresh open, deep defaultDir pre-selection, level-2 collapse,
top-change-to-leafless-dir, "+ New directory…" + name input, manual
drill into a no-children leaf.

The store API is unchanged — `saveWorkflowEntry(path, wfName, graph)`
still takes a path array, and the cascade just produces the same array
through interactive selection instead of a flat dropdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sidebar): right-click Archive folder → Delete archive (N items)

Adds a context menu to the synthetic Archive folder that bulk-removes
every archived workflow in its parent directory. Active workflows at the
same level are untouched. The menu only appears when archivedCount > 0
(Archive folder is synthetic and only renders when there are archived
workflows present), so the no-op path is unreachable in practice — the
clearArchive function still defends against it (returns false if no
archived entries to remove or directory missing).

User-asked gap from the subdirectories review. Other adjacent gaps
considered:
- Restore-all from archive: SKIP. Each archived entry has a
  "(archived <ts>)" name suffix; bulk-restoring leaves messy long names
  AND can collide with the still-active version. Per-item is the right
  granularity.
- Move a real Directory across paths: SKIP for this PR. Useful but needs
  cycle prevention + path-prefix updates; separate scope.
- Move workflow to a NEW directory inline: SKIP. Two-step workaround
  (right-click → Create subdirectory, then move) works.
- Move synthetic Archive folder: N/A. The Archive folder is auto-derived
  from `archived: true` flags; nothing to move.

Verified via Node dry-test (deleted): clearArchive correctly removes
only archived entries, preserves active ones, returns false on
empty-archive and missing-dir paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): pull 'Base on existing' candidates from ancestors too

Reported regression: navigating into an empty subdirectory (e.g.
UP-scale → seedvr2) disabled "Use existing name" / "Modify existing
name" because getActiveWorkflowsInCurrentDir only looked at the leaf
dir's workflows. With nested dirs, leaf dirs are often empty (just
created via right-click), making those actions unreachable for the
common "save a variant of an existing workflow into a new subdir" flow.

Fix: rename to getCandidatesForBase and walk the destination path from
deepest up to root, collecting active workflows at every level. Dedup
by name (deepest wins). The dropdown shows the bare name when the
candidate is from the current dir, or "name  ·  in <path>" when it
comes from an ancestor — so the user knows which dir they're basing on.

The submit path is unchanged: baseSelect.value is still the bare name,
which feeds the workflow-name field for "Use existing"/"Modify existing"
actions. The Save destination path is whatever the cascade resolves to,
not the ancestor path — so basing UP-scale/seedvr2's new save on
"Hires-FIX-wanT2V-v01  ·  in UP-scale" creates UP-scale/seedvr2/Hires-
FIX-wanT2V-v01 (no archive happens because the destination didn't have
that name before).

Verified via Node dry-test (deleted): empty subdirs now expose the
parent's active workflows; deep empties expose every ancestor's actives;
non-empty leaves show their own first then ancestors with path
disambiguation; archived entries excluded; alphabetical within each
level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sidebar): drag-and-drop moves in workflows tree (Tier 1)

User: "if we can fix this [DnD] then everything is fixed". Tier 1 ships
the moves-only piece — drag-to-move workflows, drag-to-nest dirs, drag
onto Archive to archive. No custom-ordering schema change (sort within a
level stays alphabetical); that would be Tier 2.

Drag/drop semantics:
  workflow row  → directory row    move workflow (no-op if same dir)
  workflow row  → Archive folder    archive in that dir (move first if
                                    cross-dir, all in one persist)
  directory row → directory row    nest dragged dir as child of target

Cycle prevention in moveDirectory: rejects same-parent, dropping a dir
into itself, or into any of its descendants. Plus collision (target
already has a sibling with that name) and reserved-name "Archive" at
non-root.

Implementation:
  - workflows_store.js: new moveDirectory(srcParentPath, name,
    dstParentPath) with isPathDescendantOrSame() helper.
  - constants.js CSS: grab/grabbing cursor on draggable rows; blue
    outline + tinted background on .koolook-drop-target.
  - tree.js: decorateDraggable(row, payload) and decorateDropTarget(row,
    target) helpers; handleDndDrop dispatcher; threaded through
    makeFolderRow / makeWorkflowLeafRow / buildFolder / makeSectionCtx
    so renderGatheredDir can attach DnD opts via the section ctx.

DnD uses the native HTML5 Drag-and-Drop API (no libs). MIME type
"application/x-koolook-row" filters out file/external drags.

Verified via Node dry-test (deleted) over 7 scenarios:
  1. legal cross-parent move (Type-A from UP-scale to Depth)
  2. cycle: parent → its child → blocked
  3. same-parent → blocked (no-op)
  4. cycle: dir → its own descendant → blocked
  5. name collision in destination → blocked
  6. legal sibling-becomes-child nest
  7. moving a dir literally named "Archive" to non-root → blocked

Visual feedback: drop targets get .koolook-drop-target on dragover
(blue outline + tinted background), removed on dragleave/drop. Failed
drops surface "Could not move (cycle, collision, …)" toast through the
existing persistMutation onNoOp path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): hide Action dropdown entirely when no base to act on

User feedback: when saving into a directory whose path has zero active
workflows in any ancestor (genuinely empty case), the Action dropdown
showed three options with "Use existing name" and "Modify existing
name" disabled. Disabled <option> renders subtly across browsers — most
users just see "New name" with no obvious why. They proposed two
options: (1) gray out clearly, or (2) hide the dropdown entirely. Going
with (2) — Action only matters when a base candidate exists; without
one, the only useful flow is "type a name and save," which the
Workflow Name field already provides.

So now:
- hasBase=true (current dir or any ancestor has active workflows) →
  full Action picker, all three options enabled.
- hasBase=false (truly empty path, all the way up to root) →
  Action label + dropdown hidden; underlying actionSelect.value pinned
  to "new" so submit takes the by-name path through the Workflow Name
  field.

The ancestor-pull fix from e7da864 means this empty branch only fires
for genuinely-fresh paths (a brand-new top-level + subdir created
together with no workflows anywhere). Common case is hasBase=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): apply verified PR-review findings on #54

Verified review (3 review agents → synthesizer → verifier) on the
recursive-subdirectories + Kforge Labs PR flagged 3 HIGH / 1 MEDIUM /
1 LOW. All confirmed; no false positives. Applying every actionable
fix in one commit.

[HIGH] CHANGELOG disclosure for two undocumented Save-modal shifts:
  1) Base on existing now walks ancestor paths leaf-up, dedupes deepest-
     wins, labels ancestor entries "<name>  ·  in <path>".
  2) Action dropdown is hidden (not just disabled) when no base
     candidate exists; underlying value pinned to "new" so submit takes
     the by-name path.
Both behaviors landed in late commits (e7da864 and cd9b2c3); the [Unreleased]
entry now describes them under ### Changed alongside the Kforge Labs
rebrand bullets.

[HIGH] Self-drop directory misleading toast (tree.js handleDndDrop):
The same-parent guard caught dir-on-its-own-parent but missed dir-on-
its-own-row, because the dir's row carries `target.path =
[...srcParentPath, name]` which doesn't equal `srcParentPath`.
moveDirectory's cycle check would still reject the move, but the user
got "Could not move directory (cycle, name collision, or invalid
target)" for what is just a no-op gesture. Adds a second early-return
matching `[...srcParentPath, dirName]` against `target.path` so
self-drops are silent.

[MEDIUM] addDirectory(undefined, "x") crash hardening: moveDirectory
already had `Array.isArray` guards on its parent-path inputs;
addDirectory lacked the equivalent and would throw on
`parentPath.length` if a future caller ever passed `undefined`. Adds
the symmetric guard.

[LOW] Drops the dead `isArchiveReservedName` export — the predicate
was never imported. Reserved-name enforcement remains inline in every
mutator that creates or renames a directory. A short comment notes the
removal so future contributors know to revive the export only when a
caller (e.g. modal pre-validation) actually needs it.

Verified via Node dry-test (deleted) covering:
  - addDirectory(undefined / null / "string" / []) — all behave correctly
  - Self-drop on root 'A' → "no-op-self-drop" (silent)
  - Legal cross-parent move 'A' → 'B' → "moved"
  - Self-drop on nested 'B/A' → "no-op-self-drop"
  - Drop 'B/A' onto own parent 'B' → "no-op-same-parent"

Spec alignment: every fix was verified against PR intent. The two
Save-modal shifts are organic late-stage refinements, not regressions —
disclosure is the only ask and is now delivered. The self-drop guard
and Array.isArray hardening tighten contracts that were already the
clear intent. The dead-export drop is housekeeping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:33:54 +02:00
malkuthroandClaude Opus 4.7 24dee7ee1b chore(skills): rename project /menu to /menu-comfy to avoid global collision (#53)
The project's `menu` skill at `.claude/skills/menu/` shared the same `/menu`
trigger as the user-level `~/.claude/skills/menu/` skill, so the global one
won and the project inventory never surfaced. Renamed the project skill to
`menu-comfy` so both can coexist.

- `git mv .claude/skills/menu .claude/skills/menu-comfy`
- Frontmatter `name`, header, trigger phrases, output example, and procedure-
  step reminder all switched to `menu-comfy` / `/menu-comfy`.
- Two intentional references to the global `/menu` retained — they document
  the deliberate split so future contributors don't "fix" them back.
- `docs/maintainers/README.md` companion-skills table row updated, with a
  one-line note about the collision split.

Behavior change: `/menu-comfy` now lists project skills; `/menu` continues
to list the global curated set. Docs-only — no runtime/code change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:07:57 +02:00
malkuthroandClaude Opus 4.7 e03523faf9 refactor(web): split koolook_sidebar.js into 7 sidebar modules (#51)
* refactor(web): split koolook_sidebar.js into 7 sidebar modules

Pure mechanical split — zero behavior change. Implements the design from
issue #49 (common-case layout with patchCanvasMenu extracted).

Old: web/koolook_sidebar.js (1973 lines, monolithic)
New: web/koolook_sidebar.js (48-line entry) + 7 modules in web/sidebar/

  - constants.js (130) — REPOS, TAB_*, storage keys, event names, URLs,
    ensureStyle, toast, compareNames
  - picks_store.js (117) — localStorage user picks: load/save/add (tri-state)
    /remove/notify/seed/export
  - workflows_store.js (347) — workflowsCache + persistMutation + snapshotCache
    + commit + persist*/load*/seed* + in-memory ops; cache is module-private
  - canvas_io.js (192) — serialize*/canvasIsNonEmpty/loadWorkflowOntoCanvas
    /insertNode/getSelectedNodeTypes
  - modals.js (384) — makeModalShell + showInputModal/showConfirmModal
    /showSaveWorkflowModal/showContextMenu
  - canvas_menu.js (45) — patchCanvasMenu (idempotent LGraphCanvas patch)
  - tree.js (825) — pathStates (module-private) + gatherers + DOM row
    factories + buildFolder + workflow/dir context menus + rebuildTree
    + renderPanel

Encapsulation invariants verified:
  - workflowsCache: zero references outside workflows_store.js
  - pathStates: zero references outside tree.js
  - All 8 files pass node --check
  - CSS string byte-identical to original
  - URL constants resolve to same targets after the extra `..` adjustment

Documentation updated: dev-iteration-loop.md and workflows-sidebar.md
now point pathStates references at web/sidebar/tree.js. Other doc
references to koolook_sidebar.js as the entry remain accurate.

Closes #49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(web): drop unreachable defensive guard in gatherWorkflows

Review feedback on #51: the `if (!dir) continue;` after `dirOf(dirName)`
is unreachable — `listDirectoryNames()` enumerates the same module-private
`workflowsCache.directories` keys that `dirOf()` reads, synchronously,
within a single event-loop turn. Removing the guard restores strict
"byte-identical move" semantics for the file-split refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:47:20 +02:00
malkuthroandClaude Opus 4.7 cc6bc47866 docs+tooling: VAE user guide + versioning ref + 3 skills + preflight CI (#48)
* docs+tooling: VAE user guide + versioning ref + 3 skills + preflight CI

Documentation
- New user-guide tree: docs/user_guide/nodes/radiance_koolook_v2_3_3/
  with README, per-node pages for Easy_hdr_VAE_encode/decode, an
  encode/decode pairing guide (4 worked examples + ASCII connectivity
  diagram), and an HDR modes deep-dive (per-pixel comparison table).
  Folder name mirrors the code path under forks/radiance_koolook/versions/.
- New cross-cutting reference: docs/reference/versioning.md explains the
  three independent version axes (pack version, fork wrapper version,
  upstream pinned commit), when each one bumps, and which skill/doc
  owns it. Plus the v1/v2 node-ID suffix policy.
- Cross-links updated in docs/user_guide/README.md, docs/reference/
  README.md, docs/maintainers/README.md.

Skills (all under .claude/skills/)
- docs-sync — controlled find-and-replace for version tags across docs,
  code, and manifests; --scope and --dry-run flags.
- menu — lists all project skills with one-liners.
- preflight-release — companion doc for tools/preflight_release.py.

Pre-flight tooling
- tools/preflight_release.py — four checks, exit-code-driven for CI:
  (1) static AST extraction of NODE_CLASS_MAPPINGS keys (no torch needed),
  (2) stub-VAE dispatch test verifying all 5 rank/VAE-type branches,
  (3) drift check against ComfyUI-Manager's extension-node-map.json
      (catches the issue #44 phantom-nodes class of bug automatically),
  (4) saved-workflow regression check across tests/workflows/*.json.
- .github/workflows/ci.yml — new "Pre-flight Release" job runs checks
  1, 2, and 4 on every PR; check 3 (manager-meta) skipped pending the
  upstream issue #44 fix.
- tests/workflows/ — 2 starter fixtures (Depth-Anything3, Hires-FIX-wanT2V)
  + README documenting the contribution convention. Coverage grows as
  new fixtures are added; no code changes needed.

All four offline pre-flight checks pass on this branch. The
manager-meta check correctly identifies the 39 phantom IDs documented
in issue #44.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(preflight): satisfy ruff lint + bandit security gate

* fix(preflight): address blocking review findings

Three fixes from /review-pr feedback on this PR:

1. Sanitize tests/workflows/*.json — removed 4 unique absolute paths that
   leaked internal job/client info ('baconx/Toy-Hix/9110',
   'menneskehavn_3029', etc.). Replaced with placeholders like
   'path/to/sequence_a' and 'path/to/source_video'. Both fixtures still
   parse as valid JSON; grep for the old patterns returns zero hits.

2. Broaden the Koolook-ID heuristic regex from '^easy_[a-z]' to
   '^easy_[A-Za-z]'. The old pattern excluded 'easy_ImageBatch' (capital I
   after the underscore), which would have silently bypassed the
   workflow-fixture stability check for that node. Verified the new
   pattern correctly matches it.

3. Wrap check_dispatch() body in try/finally and track every sys.modules
   key the function injects in a list, so cleanup runs on the success
   path AND on any exception during exec_module / VAE-constructor
   execution. Previously only the three torch* stubs were popped, never
   the three _pf_pkg* entries — and even those weren't popped on
   exception paths. Verified zero leaked modules after the function
   returns.

The five dispatch test cases were factored into a helper
_check_dispatch_run_cases() so the try/finally wrapping stays focused on
module injection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(preflight): address remaining review findings

Follow-up to 7844037 — picks up all the HIGH / MEDIUM / LOW review
findings that were deferred. Pre-flight still passes locally.

HIGH
- Extract check_dispatch (~195 lines) to tools/_preflight_dispatch.py.
  Main script drops from 680 → 508 lines. Module exposes a single
  run(verbose, repo_root) -> (bool, list[str]) entry; main script keeps
  the [2/4] header + PASS/FAIL line.
- Install _HttpsOnlyRedirectHandler that re-validates each redirect
  hop's scheme. Closes the gap that urlopen would otherwise follow a
  30x to http:// or file:// silently. The Bandit B310 suppression
  comment now accurately describes the protection.
- check_static docstring now honestly states it only walks
  NODE_CLASS_MAPPINGS literal dicts and does NOT apply
  NAMESPACE_SUFFIX / SKIP_VERSION_SUFFIX logic. Notes that the workflow
  check (4) is the safety net that surfaces drift the day a non-skipped
  ID lands.
- check_workflows now FAILS when the fixtures dir exists with a README
  but contains no *.json files — previously silently SKIPPED. If
  someone deletes fixtures or typoes the dir, that's a regression in
  test coverage and should fail loudly.

MEDIUM
- check_manager_meta validates the upstream entry shape before diffing.
  Previous code silently produced an empty set if the schema changed,
  reporting every one of our IDs as 'missing'. Now SKIPs with a clear
  message when shape is unexpected.
- check_workflows accumulates total_koolook_refs inside the main loop;
  drops the redundant second-pass that re-read every fixture file just
  to compute the summary count.
- _extract_workflow_node_types uses explicit detection: a canvas-format
  file with nodes: null no longer falls through to API-format and pulls
  class_type from unrelated keys.
- check_static return type simplified from (bool, set, list) to
  (bool, set). Third element was discarded at every call site.
- check_workflows now does two-pronged Koolook-ID matching: heuristic
  (catches drift even if static missed an ID) plus actual_ids (catches
  IDs that don't fit the heuristic, e.g. a future Koolook node without
  Easy / easy_ / koolook in its name).

LOW
- ANSI escape codes named in a constants dict (_ANSI["green"] etc.)
  instead of bare string literals.
- Removed dead 'cases: list[tuple[str, callable]] = []' initializer
  (was shadowed three statements later).
- check_static's per-ID listing now gated behind --verbose, matching
  check_dispatch and check_workflows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:31:36 +02:00
malkuthroandClaude Opus 4.7 b7b34de7e2 feat(sidebar): curated nodes + workflows tab with archive (#45)
* feat(sidebar): add curated nodes sidebar tab with favorites

New "Curated Nodes" sidebar tab. Shows Koolook nodes auto-grouped by
subcategory under a "Nodes (favorites)" root. Users add favorites from
any installed pack from inside ComfyUI via:

- "+" button: adds the currently selected canvas node(s)
- Right-click any canvas node -> "Add to Curated Sidebar"

Picks persist in browser localStorage (per-browser, survives ComfyUI
restarts). Hover any entry -> "x" to remove. Search filter auto-expands
all folders so matches stay visible; pack/category folders are
collapsed by default otherwise to keep the panel compact.

"Download" export button copies current picks as JSON suitable for
web/curated_defaults.json -- committed to ship as the package's
defaults. On first ComfyUI load with empty localStorage, the seeder
reads curated_defaults.json into localStorage so fresh installs get the
curated set pre-loaded. Existing users keep their localStorage
untouched.

Pure JS, no build step. Uses the documented
app.extensionManager.registerSidebarTab API (stable since
ComfyUI_frontend v1.2.4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(sidebar): document curated-defaults workflow + seed initial 9 picks

Adds docs/maintainers/curated-sidebar.md covering the full Export ->
paste -> commit flow for shipping distributed favorites with the
package, including storage-key reference, reset procedure, and the
intentional "seed once per browser" semantics.

CLAUDE.md gets a short pointer so the trigger ("update curated
defaults" or pasting an exported picks JSON) is discoverable on the
agent side too. Maintainer docs index linked.

Seeds web/curated_defaults.json with the maintainer's first 9 picks
(GetImageSizeAndCount, GetNode, ImageScaleBy, LoadEXR, NukeConstant,
NukeOCIOColorSpace, SaveEXRFrames, Text Multiline, easy showAnything).
Fresh installs from this version forward will get them pre-loaded into
their localStorage on first ComfyUI launch; existing users' picks are
untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(sidebar): trim curated-sidebar.md to a one-screen quick reference

Drops the verbose data-flow diagram and the localStorage-vs-userdata
rationale. Keeps the four action steps, the storage-key map, the reset
block, and the four "things that surprised us" footnotes capturing the
non-obvious bits (paste is manual, other users can't reach the repo,
seed-once-per-browser, no publish on web/ pushes). 81 lines removed,
20 added; same file path so existing links still resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sidebar): workflows section with archive + iteration docs

Adds a Workflows section to the Curated Nodes sidebar tab, sharing the
search field and tree pattern with the existing nodes feature.

Capabilities:
- Save entire canvas or current selection as a workflow.
- Save modal: New name / Use existing name (archive previous) /
  Modify existing name. The Workflow-name field is shown only when
  needed; "Base on existing" dropdown drives prefill for Use/Modify.
- Auto-archive on same-name save: previous version renamed with
  timestamp suffix and flagged archived; new entry takes the slot.
  Archive sub-folder appears in each directory only when populated and
  is rendered above the active list, so the latest sits at the bottom.
- Right-click on workflow: Load / Rename / Move to other directory /
  Move to archive (or Restore from archive) / Delete.
- Right-click on directory: Rename / Delete (with confirm if non-empty).
- Loading a workflow names the tab via loadGraphData's 4th arg
  (verified against ComfyUI_frontend src/scripts/app.ts) so the tab
  flips from "Unsaved Workflow (N)" to the saved name.

Storage: ComfyUI /userdata/koolook_workflows.json (primary), with a
localStorage fallback if /userdata is unreachable. Distributed defaults
seeded from web/workflow_defaults.json on first ComfyUI load with empty
workflow data -- same one-time-per-install pattern as
curated_defaults.json.

Other panel changes:
- Three labeled rows above the tree (search; NODES action bar;
  WORKFLOWS action bar), with thin dividers between them and before
  the tree.
- Folder expansion state preserved across re-renders via a pathStates
  Map; saving never collapses the directory you were viewing.
- Alphabetical (case-insensitive) sort everywhere via a single
  compareNames helper -- pack names, subcategories, directories,
  workflow names, dropdown options.
- "Use existing name (archive previous)" action label includes the
  parenthetical hint about archive behavior.

Docs:
- New docs/maintainers/dev-iteration-loop.md captures the
  dev-sync->restart->verify pattern, agent trigger phrases, persistence
  boundaries, and push/publish gates. First read when returning to UI
  work after a break.
- New docs/maintainers/workflows-sidebar.md covers workflow storage,
  action semantics, archive behavior, and reset procedure.
- docs/maintainers/README.md indexes both.
- CLAUDE.md "dev-sync" section now points at the iteration-loop doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): address review findings — seed gating + commit checks

Targets the data-integrity findings from the multi-agent PR review:

- BLOCKING: seed flag is now only set after the underlying save/persist
  succeeded. `saveUserPicks` returns a boolean; both seeders bail out
  (leaving the flag unset for next-load retry) on storage failure.
  Previously a transient storage failure during first-load could
  permanently strand a user with an empty store and the seeded flag set.

- BLOCKING: `normalizeWorkflowsStore` now strictly validates each
  workflow entry — drops entries missing a graph object, coerces
  `archived` to a strict boolean (so a stray "false" string can't
  accidentally hide a workflow), and logs the dropped count.

- HIGH: save handlers now check `commit()`'s return value and surface a
  "Save failed" toast when both /userdata and the localStorage fallback
  rejected the write. Previously the user saw "Saved" even though
  nothing was persisted. The canvas and selection handlers share a
  `saveAndToast` helper to keep the failure path consistent.

- HIGH: `saveCanvasBtn` now checks `canvasIsNonEmpty()` before
  serializing. A `serialize()` throw is no longer misreported as
  "Canvas is empty." — distinct toasts for distinct failure modes.

- HIGH: `loadWorkflowsStore` now logs (instead of silently swallowing)
  parse failures on the localStorage fallback, and warns when /userdata
  is reachable but a stale fallback blob still exists from an earlier
  outage — gives the user a recovery breadcrumb.

- HIGH: `loadUserPicks` logs parse failures so a corrupt picks blob is
  visible in the console before the next save overwrites it.

Skipped from the review with rationale:
- multi-tab seed race: sub-second window, both tabs write identical
  defaults, no observable wrong state — navigator.locks not worth it
- splitting `koolook_sidebar.js` and decomposing `renderPanel`:
  refactor scope, deferred to follow-up
- `loadGraphData` rename comparison: agent's claim was wrong; existing
  `wf.filename !== wfName` matches the upstream `filename` shape (bare
  name, no extension) we verified earlier against ComfyUI_frontend
  v1.44.x source

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): second-pass review fixes — rollback, idempotency, corrupt-file handling

Addresses the residual findings from the post-625aa97 review pass.

BLOCKING:
- patchCanvasMenu is now idempotent. Adds a __koolookCuratedPatched
  sentinel on LGraphCanvas.prototype so a soft-refresh / re-import
  doesn't stack duplicate "Add to Curated Sidebar" entries on every
  node's right-click menu. (LGraphCanvas is shared with every other
  pack, so non-idempotent prototype mutation was visible globally.)

HIGH:
- persistWorkflowsToServer now returns "server" | "fallback" | false
  instead of just true/false. seedWorkflowDefaultsIfNeeded only marks
  WORKFLOWS_SEEDED_KEY when the seed actually reached /userdata —
  previously a fallback-only seed (when /userdata was unreachable on
  first install) marked the user as "seeded" and they would have an
  empty server file forever after recovery.

- All 8 commit() callsites that mutate workflowsCache (saveAndToast,
  move, archive, restore, rename workflow, delete workflow, rename
  directory, delete directory + delete-non-empty-directory, create
  directory) now go through a new persistMutation helper that:
  - snapshots the cache before mutation
  - bails (toast no-op) if mutate returns false
  - rolls back the cache + re-renders if commit() returns false
  - toasts persistFailedMessage so the user isn't told a write
    succeeded when both /userdata and the localStorage fallback
    rejected it
  Pairs with snapshotCache() helper for explicit snapshot/restore.

- addToMyPicks tri-state ("added" | "duplicate" | "failed") so callers
  can distinguish "already in favorites" from "save failed" on
  localStorage quota / private-mode failure. Both call sites (the +
  toolbar button and the right-click "Add to Curated Sidebar" menu
  entry) updated to handle all three states.

- fetchWorkflowsFromServer now distinguishes a network failure from a
  parse failure of an existing-but-corrupt /userdata file. Returns
  SERVER_FILE_CORRUPT (a Symbol) on parse error; loadWorkflowsStore
  surfaces a toast and returns { corrupt: true }; setup() skips
  seedWorkflowDefaultsIfNeeded entirely so we don't auto-overwrite a
  recoverable-but-unreadable file with stock defaults.

- loadWorkflowOntoCanvas snapshots the current canvas before
  app.loadGraphData and restores it on throw. Previously a workflow
  with a now-unregistered node type could destroy the canvas partway
  through and leave the user with neither the old graph nor the new.

- Stale localStorage fallback detection (when /userdata is reachable
  but a fallback blob from a previous outage is still present) now
  surfaces a toast in addition to the console warning, so a user who
  doesn't have DevTools open gets a recovery breadcrumb.

MEDIUM:
- Modal escape-handler leak fixed via centralized close(). Every
  dismissal path (Escape key, overlay click, Cancel/OK calling
  overlay.remove()) now tears down the document-level keydown listener.
  Existing call sites that use overlay.remove() keep working — the
  remove method itself routes through cleanup.

Skipped from the review with rationale:
- subcategoryFor cosmetic mismatch fallback at L1000-1008 — silent
  rewrite is rare-path defensive, not load-bearing; defer.
- Optional toast on seedDefaultsIfNeeded storage failure — console
  warning is sufficient for a development-time scenario; defer.
- File size / module split (1864 lines) — refactor scope, deferred to
  follow-up per prior commit message.
- Magic sentinel strings ("__new__", action enum strings) — pure
  cleanup; defer.
- dataset.expanded boolean-string compare — pathStates Map already
  carries booleans; cleanup defer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:33:53 +02:00
malkuthroandClaude Opus 4.7 ee8ccefdf8 docs(reorg) + chore: roll [Unreleased] forward + bump 0.1.7 (#37)
Bundles two changes for v0.1.7:

1. PR #36 (already on main, just rolling [Unreleased] -> [0.1.7]):
   suffix all node display names with " (Koolook)" so they group
   together in ComfyUI's node-add search. Class IDs unchanged.

2. Docs reorg:
   - git mv Glossary.md            -> docs/reference/glossary.md
   - git mv RELEASING.md           -> docs/maintainers/releasing.md
   - Three audience buckets under docs/: user_guide/ (end users),
     reference/ (lookups), maintainers/ (project ops). Each gets a
     short index README.md.
   - Top-level docs/README.md as the navigation entry point.
   - New: docs/maintainers/registry-api.md captures the Comfy Registry
     undocumented version-management API (deprecate/yank/status enum,
     auto-deprecation behavior, curl recipes, optional GH Actions
     workflow YAML for codified mgmt without exposing the token).
   - New: docs/maintainers/node-versioning.md codifies the rules from
     the v0.1.5 PR-review discussion for changing INPUT_TYPES /
     RETURN_TYPES / class names without breaking saved workflows.
   - Imported the existing untracked docs/ images that the maintainer
     started locally (node inventory screenshot, Easy Image Batch
     helper image).
   - Cross-references updated in README.md, CLAUDE.md, the issue
     template, and .claude/skills/add-external-fork/SKILL.md.

Root drops from 6 -> 4 markdown files. The four that stay (README,
LICENSE, CHANGELOG, CLAUDE.md) are the ones that *must* live there for
GitHub / Comfy Registry / tooling / Claude Code reasons.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 23:26:46 +02:00