Commit Graph
29 Commits
Author SHA1 Message Date
malkuthro a33ce9ec30 fix(sidebar): clamp long node preview descriptions 2026-05-06 08:48:13 +02:00
malkuthroandClaude Opus 4.7 75b7e76f11 feat(sidebar): node-preview fidelity — read nodeData, 5-col grid, widget pills (#78) (#82)
Two changes that ship together because the visual rework was masking the
real bug:

1. `readSlots` (and the title/category/description reads in
   `renderPreviewCard`) were looking at static `nodeClass.INPUT_TYPES` /
   `RETURN_TYPES` properties, which **don't exist** on
   ComfyUI-registered nodes — ComfyUI parks the original V1 def at
   `nodeClass.nodeData` (set by `litegraphService.ts`'s
   `registerNodeDef` right before `LiteGraph.registerNodeType`). Every
   card was rendering as just header-and-badge because the slot arrays
   came back empty for every ComfyUI node. Reading from `nodeData`
   first (with the old property names as a legacy fallback for any
   non-ComfyUI registered nodes) fixes the empty-card class.

2. Visual rework to look like a real node mock instead of a generic
   info card. Mirrors upstream NodePreview.vue more faithfully: flat
   header row (small colored dot + display name) over a red PREVIEW
   badge; slot rows in a 5-column grid (`[dot] [input-name] [spacer]
   [output-name] [dot]`) so inputs and outputs line up horizontally as
   parallel sockets; widgets rendered as individual rounded pills with
   `◀ name [spacer] value ▶` chrome to mimic LiteGraph widget UI.

Closes #78

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:16:42 +02:00
malkuthroandClaude Opus 4.7 d021e8c8da feat(sidebar): theme-classifier rework + Nodes-row polish (#80)
* revert(sidebar): drop drag-link-release menu priority patch

The patch in `web/sidebar/connection_menu.js` was reorder-only — it sorted
existing menu items so curated picks floated to the top. In practice
ComfyUI's drag-link-release menu has its own pre-filter that surfaces only
~4 stock nodes per socket type (e.g. for an IMAGE-compatible drop target:
VAEEncode, VAEEncodeForInpaint, SaveImage, PreviewImage). Most user
picks never appear in that filtered list, so the reorder had nothing to
hoist and the feature was invisible in practice.

A useful version of this would have to ADD picks to the menu — walk the
user's curated picks, type-match each against the dragged socket, inject
matches as new menu items — rather than just reorder. That's a larger
patch and out of scope for tonight; filing as a follow-up rather than
shipping dead code.

Removed: `web/sidebar/connection_menu.js`, the `patchConnectionMenu`
import + call in `web/koolook_sidebar.js`, and the corresponding
[Unreleased] CHANGELOG entry.

Kept: spotlight-on-add, modal drag-out fix, and the `pinExpanded`
buildFolder fix from #76 — all independently useful.

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

* feat(sidebar): theme-classifier rework + Nodes-row polish

Replaces the previous "category mode" (canonical-segment first-segment
grouping, which produced a fragmented tree of `image` / `images` /
`Image-Save` / `KJNodes/image` / `Koolook/Image` for the same semantic
theme) with a broad keyword classifier that folds anything mentioning
an image-related word into a single "Image" bucket regardless of source
pack or category casing. Picks-only; flat list inside each theme; falls
back to pack-prefix-stripped first segment when no classifier matches.

Five coordinated changes shipped together:

1. Theme classifier (new). `THEME_CLASSIFIERS` is an ordered list of
   13 broad themes (Image / Video / Mask / Audio / Model / Sampler /
   Conditioning / Text & Prompt / Math & Logic / Pipeline & Batch /
   I/O / Camera / Utility) with regex pattern arrays. `extractTheme`
   builds a search corpus from packLabel + category + display title +
   type ID and walks the classifiers in order — first match wins.
   First match for "Image Comparer (rgthree)" is "Image" (not Math)
   because Image is earlier in the list. Symbols touched in tree.js:
   `gatherNodesByTheme`, `extractTheme`, `classifyTheme`,
   `THEME_CLASSIFIERS`, `emitThemeChildren`, `collectFlatFromThemes`,
   `countNodesInThemes`. Replaces `gatherNodesByCategory`,
   `emitCategoryTreeChildren`, `collectFlatFromCategoryTree`,
   `countNodesInCategoryNode`, `ensureSyntheticBucket`.

2. Pack-name badges removed. `makeNodeLeafRow` no longer accepts a
   `packBadge` parameter; the badge rendering block is gone; all
   callers (3 sites) updated; CSS rule `.koolook-pack-badge` removed
   from constants.js. The breadcrumb prefix on search-flatten rows
   already conveys origin in a cleaner form, and theme mode by
   definition doesn't care about source pack.

3. Mode-toggle icons swapped. Repo mode: `pi-database` -> `pi-sitemap`
   (now actually tree-shaped). Theme mode: `pi-sitemap` -> `pi-bars`
   (sandwich). Better icon-to-meaning mapping.

4. Default mode flipped to "category" (theme). Newcomers more often
   think "where are my image nodes" than "which pack is this from."
   Existing users with `koolook.groupMode.v1` already in localStorage
   keep their saved choice.

5. Layout polish. `+` button uses a `pi pi-plus` icon at 12px (matches
   toolbar icon-button height) with a new dusty-green accent class
   `koolook-add-btn-green` (rgba 120/165/100). New divider between the
   search field and the Nodes action bar. `koolook-sidebar` gained
   `padding-top: 10px` so the Snapshot row no longer hugs the panel
   top edge.

CHANGELOG [Unreleased] updated with Changed entries for items 1+2.

Note: a CSS comment containing backticks (`koolook-icon-btn`) inside
a JS template literal silently broke module load on the first attempt —
backticks closed the template early, leaving "koolook-icon-btn" as a
stray identifier the browser rejected with "Unexpected identifier
'koolook'". Fixed by removing the backticks from the comment. `node
--check` did not catch this; the only reliable check is browser-side.

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

* chore(sidebar): address fresh-eyes review nits on theme classifier

Three NITs + one IMPORTANT comment from the PR #80 review pass:

- IMPORTANT: Add a code comment on `\bget\b` / `\bset\b` in the Utility
  classifier warning future editors not to generalize to unbounded
  `/get/` / `/set/`. Current narrow forms are intentional — they're
  the last-resort net for trivial getter/setter nodes that didn't hit
  any earlier classifier.
- NIT: Fix the `classifyTheme` doc-comment example for "Image Comparer"
  to be technically accurate. The previous wording claimed Math's
  `/\bcompare\b/i` would match "Comparer" — it wouldn't, because the
  trailing word boundary fails on "Comparer". The actual reason it
  lands in Image is the `/image/i` substring hit on the "Image" word
  in the corpus, not ordering.
- NIT: Drop `.toLowerCase()` on the search corpus in `extractTheme`.
  Every classifier pattern carries the `/i` flag, so the explicit
  case fold was redundant work.
- NIT: Remove dead `(nc && ...)` defensive checks after the early
  return on `!nc`. Per CLAUDE.md "no defensive coding for impossible
  cases" — `nc` is guaranteed non-null on the lines below the early
  return. Same in `gatherNodesByTheme` where `nc` is fetched after
  `extractTheme` already filtered out unresolved picks; added a comment
  noting the invariant.

Behavior unchanged. All pure correctness/style polish.

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 22:41:14 +02:00
malkuthroandClaude Opus 4.7 215616f546 revert(sidebar): drop drag-link-release menu priority patch (#79)
The patch in `web/sidebar/connection_menu.js` was reorder-only — it sorted
existing menu items so curated picks floated to the top. In practice
ComfyUI's drag-link-release menu has its own pre-filter that surfaces only
~4 stock nodes per socket type (e.g. for an IMAGE-compatible drop target:
VAEEncode, VAEEncodeForInpaint, SaveImage, PreviewImage). Most user
picks never appear in that filtered list, so the reorder had nothing to
hoist and the feature was invisible in practice.

A useful version of this would have to ADD picks to the menu — walk the
user's curated picks, type-match each against the dragged socket, inject
matches as new menu items — rather than just reorder. That's a larger
patch and out of scope for tonight; filing as a follow-up rather than
shipping dead code.

Removed: `web/sidebar/connection_menu.js`, the `patchConnectionMenu`
import + call in `web/koolook_sidebar.js`, and the corresponding
[Unreleased] CHANGELOG entry.

Kept: spotlight-on-add, modal drag-out fix, and the `pinExpanded`
buildFolder fix from #76 — all independently useful.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:35:04 +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 ced6492257 feat(sidebar): category mode + flatten-on-search + hover preview (#74)
* feat(sidebar): category mode + flatten-on-search + hover preview (#73)

Three navigation/discoverability improvements to the Kforge Labs sidebar's
Nodes section, shipped together:

1. Category-first grouping mode. Segmented toggle in the Nodes action row
   (📦 Repository / 🌐 Category) re-groups picks by their node-class
   CATEGORY path instead of by repo, with canonical-key normalization
   (`lower().replace(/[\s_-]+/g, "")`) so `Loaders`/`loaders` and
   `Image/Upscaling`/`image/upscaling` collapse into one folder. Synthetic
   `(unresolved)` and `(uncategorized)` buckets surface picks whose pack
   isn't loaded or whose class has no CATEGORY. Pack-name badge on
   category-mode leaves preserves the source signal. Mode persists in
   localStorage with cross-tab sync.

2. Flatten-on-search. When the search field is non-empty the Nodes
   section drops its tree structure and renders a flat list of matching
   leaves with `Cat › Subcat › Display` breadcrumb prefixes. Workflows
   and Tags sections retain their tree-under-filter behavior — the
   change is scoped to Nodes only.

3. Hover preview card. Mirrors ComfyUI's official NodePreview.vue: plain
   HTML/CSS card with a colored title bar (HSL hue from CATEGORY hash),
   two columns of type-colored slot dots (reads
   `app.canvas.default_connection_color_byType` first, falls back to
   LiteGraph's static palette), widgets with truncated defaults,
   optional description. 250ms hover debounce, viewport-flip, max-height
   cap, pointer-events: none, leak-proof teardown wired into renderTree.
   "Pack not loaded" stub for unresolved picks. New module
   `web/sidebar/node_preview.js` holds the engine; one
   `attachHoverPreview(row, type)` call inside `makeNodeLeafRow` covers
   every leaf-emit site (repo tree, category tree, flat search).

Closes #73

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

* fix(sidebar): preview review-fixes — log INPUT_TYPES errors, teardown on tab/blur, type-guard slot colors

Three small additive fixes from the multi-agent review on PR #74:

1. console.warn when INPUT_TYPES() throws so a broken pack is visible
   during debugging instead of silently rendering an empty-looking node.
   Matches the project-wide convention (picks_store.js, canvas_io.js,
   installer.js).

2. visibilitychange + window.blur listeners that call teardownPreview.
   Closes the leak path where the user navigates away mid-hover via
   keyboard sidebar-tab switch, modal stealing focus, or page hiding —
   none of which cross a row's mouseleave region, so the reactive
   teardown never fired and the pointer-events:none card sat on screen
   undismissable.

3. typeof === "string" guard on both fallback branches in slotColor so
   a misbehaving custom-node pack injecting a non-string into
   default_connection_color_byType produces a visible neutral-grey dot
   instead of a silent transparent dot.

The category_grouping.js extraction (MEDIUM finding, file split) is
queued as a separate follow-up issue per the "one PR per topic" rule
in CLAUDE.md — pure no-behavior refactor, lands cleaner on its own.

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 20:39:48 +02:00
malkuthroandClaude Opus 4.7 96b07a2c12 feat(sidebar): picks priority — drag-link menu, spotlight on add, modal hardening (#76)
* 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>

* feat(sidebar): picks priority — drag-link menu, spotlight on add, modal hardening

Three coordinated UX features for curated picks plus two underlying bug
fixes uncovered while wiring them.

1. Drag-link-release menu hoists curated picks. When a connection is
   dragged from a socket and released on empty canvas, ComfyUI's compat-
   filtered suggestion list now sorts items in the user's curated picks
   above everything else, with a "── from your picks ──" separator before
   the rest. Detection via the "Add Node" + "Add Reroute" sentinel pair
   in the menu's options (narrow enough that other ContextMenu uses are
   untouched). Purely additive — no compatible nodes are removed, only
   re-ordered. New `web/sidebar/connection_menu.js` exports a
   `patchConnectionMenu()` registered alongside `patchCanvasMenu()`.

2. Spotlight effect on add. Clicking the toolbar `+` (or the canvas
   right-click "Add to Kforge Labs") collapses every Nodes-section pack
   folder, then auto-expands the pack + subcategory the just-saved node
   lives in — pedagogical aid that helps users learn which pack each
   node belongs to. Multi-select adds light up every hit pack
   simultaneously. Duplicate adds get the spotlight too (the educational
   reminder still applies). New helper `findPackPathForType` mirrors the
   gather code's REPO-precedence-then-user-pick-fallback. New exported
   `spotlightAddedPicks(typeNames)` does the collapse + pin sequence.

3. Modal drag-out-of-input no longer dismisses the dialog. Shared modal
   shell `makeModalShell` gained a mousedown / click-intent check: a
   click on the overlay only fires dismiss when the gesture STARTED on
   the overlay too. Drag-selecting text inside an input that releases in
   the overlay's dark area used to dismiss the modal mid-edit. Affects
   every Koolook dialog. Snapshot Settings path field also gained a
   `title` tooltip so the full path is hover-readable.

Bug fixes uncovered + carried in this PR:

- `pinExpanded` paths now expand on the immediate render. buildFolder's
  resolution chain gained a `pinnedPaths` check between `forceExpanded`
  and `pathStates`. Phase 3 of `renderTree` writes pins into pathStates
  AFTER Phase 2 builds the DOM, so without this check the pin had no
  effect on the current render. Improves the existing workflow-save
  pinning (visibly correct now) and is load-bearing for the new
  spotlight feature.

CHANGELOG [Unreleased] updated with all five entries.

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 20:33:31 +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 93363cc625 feat(sidebar): snapshot library v2 — configurable filesystem path via KFORGELABS_PRESETS (#68)
* feat(sidebar): snapshot library — named presets for picks + workflows

Implements #46 item 1 (custom save location + cross-machine portability)
as a preset library: each preset is one named JSON file under
/userdata/koolook-presets/, capturing the full Kforge Labs state.

Use cases addressed
-------------------
- **Personal portability:** copy `koolook-presets/` between machines, or
  sync ComfyUI's userdata folder via Dropbox/iCloud/Drive — your kit
  follows.
- **Team / facility shared library:** multiple workstations on the same
  ComfyUI server share `/userdata/koolook-presets/` natively. One person
  publishes "facility kit", everyone else loads it.
- **One-off transfer (different servers):** Manage tab's Download button
  saves a snapshot to local disk; Upload imports it into another machine's
  library.

UI
--
New top-level **Snapshot** action row above the search field, mirroring
the Nodes/Workflows action-row pattern. Three icon buttons (Load / Save /
Manage), each opens the same modal at a different starting tab.

- Load tab: lists every preset with metadata (workflow count · pick
  count · export date). Click a row → "Replace current state?" confirm
  → restores both picks and workflows.
- Save tab: input field defaults to `preset YYYY-MM-DD`, fully editable.
  Existing-name save prompts to overwrite.
- Manage tab: per-row Download / Delete buttons + a top-of-tab Upload
  action (file picker imports a JSON snapshot from disk into the library).

Schema
------
```json
{
  "kind": "koolook-snapshot",
  "version": 1,
  "name": "Wan video kit",
  "exportedAt": "2026-05-05T08:42:13.000Z",
  "picks": [...],
  "workflows": { "directories": {...} }
}
```
`kind` + `version` for future schema migrations. Anything without
`kind === "koolook-snapshot"` is rejected on import.

Module shape
------------
- `web/sidebar/snapshot.js` (new) — server I/O on the preset directory,
  schema validation, gather/apply, download/upload helpers. UI-free.
- `web/sidebar/modals.js` — new `showSnapshotModal({initialTab, ...})`
  that takes capability functions instead of importing snapshot.js
  directly, so the UI module stays UI-only.
- `web/sidebar/workflows_store.js` — new `replaceAllWorkflows(rawStore)`
  for bulk-replace (bypasses persistMutation since the entire cache is
  being swapped) + `getAllWorkflowsForExport()` for the gather flow.
- `web/sidebar/picks_store.js` — new `setAllPicks(picks)` exported
  helper.
- `web/sidebar/tree.js` — new top action row + 3 toolbar buttons +
  one wiring helper.
- `web/sidebar/constants.js` — CSS for tab strip, list rows, manage
  buttons.

Deliberate scope cuts (per the design discussion)
-------------------------------------------------
- No continuous sync. Each save is a deliberate act.
- No conflict resolution. Same preset name written from two machines
  on the same server = last-write-wins. Documented as a known limit;
  revisit when it bites.
- No selective import (preset = full state). Merge can be a v2.
- No custom userdata path config. The `koolook-presets/` location is
  fixed; portability comes from the snapshot files themselves, not
  from rerouting storage.

Verification
------------
- `node --input-type=module --check` on all modified + new JS files —
  clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac for browser smoke-test.

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

* feat(sidebar): snapshot library now uses configurable filesystem path

Reworks PR #67 (which served snapshots from /userdata/koolook-presets/)
to expose the library through Koolook's own server-side endpoints,
making the storage location configurable via an env var. The userdata
namespace is per-install — fine for personal use, useless for the
"facility shared library" use case the user explicitly called out.

Architectural change
--------------------
- The client no longer talks to ComfyUI's `/userdata/*` API for snapshot
  files. New JS-side endpoints: `/koolook/presets/{info,list,file}`.
- Server side: new `koolook_routes.py` registers aiohttp routes on
  ComfyUI's PromptServer at custom-node load time. Path-traversal
  protection at the route boundary via a strict filename whitelist
  regex (`^[A-Za-z0-9 _.()\-]+\.json$`).
- Storage location is read from the `KFORGELABS_PRESETS` env var on
  every request (so re-launching ComfyUI with a new value picks it
  up without any cache fuss). Default fallback:
  `<comfyui-userdata>/koolook-presets/`.
- Auto-creates the configured directory on first save (parent must
  already exist — for NFS/SMB mounts the mount must be present).

Use cases this enables vs. PR #67
---------------------------------
| Use case                | PR #67 | This PR |
|-------------------------|--------|---------|
| Personal, single-machine | ✓     | ✓       |
| Personal cross-machine  | symlink-or-sync userdata | point env var at Dropbox folder |
| NFS/SMB facility share  | ✗ (locked to userdata) | ✓ point env var at mount |
| Multiple parallel libs  | ✗     | per-instance env var |
| Read-only distribution  | ✗     | ✓ (server detects, save fails cleanly) |

UI difference vs. PR #67
------------------------
- The Manage tab gains a small `i` icon at the right edge of the
  upload row. Hover tooltip shows the current library path, source
  (default vs `KFORGELABS_PRESETS` override), and writability status.
  No header line, no extra UI weight.

Files added/changed
-------------------
- `koolook_routes.py` (new, ~180 lines) — aiohttp routes + env-var
  config + filename validation. Loaded by `__init__.py` at startup;
  failure is non-fatal (the rest of Koolook still works, just the
  snapshot feature is unavailable).
- `__init__.py` — registers `koolook_routes` import-time side-effect.
- `scripts/sync_to_dev.py` — `koolook_routes.py` added to
  `RUNTIME_PATHS` so dev-sync includes it.
- `web/sidebar/snapshot.js` — server I/O section rewritten to call
  `/koolook/presets/*` instead of `/userdata/koolook-presets/*`. Drops
  the URL-encoding workaround (we own the route now, so the
  `name=<file>` query param is clean and the server validates).
- `web/sidebar/modals.js` — `showSnapshotModal` accepts a
  `getLibraryInfo` capability function; the Manage tab calls it and
  populates the info-icon tooltip.
- `web/sidebar/tree.js` — passes `getLibraryInfo` from `snapshot.js`
  to the modal.
- `.env.example` — new `KFORGELABS_PRESETS` block with macOS / Linux /
  Windows examples for personal-sync, NFS facility share, and SMB
  facility share patterns.
- `CHANGELOG.md` — Added entry rewritten for the env-var design;
  Internal entry on the new server-side module.

Why a separate v2 PR (instead of patching #67)
----------------------------------------------
The userdata-namespace design in #67 was the wrong architecture for
what the user actually wanted. Patching it to the env-var design
would have meant a near-total rewrite of #67's server I/O paths
mid-review. Cleaner: branch fresh from main, cherry-pick #67's UI
+ stores work as a base, layer the new endpoint design on top.
PR #67 will be closed as superseded once this lands.

Verification
------------
- `node --input-type=module --check` on every modified JS file —
  clean.
- `python -c "import ast; ast.parse(...)"` on `koolook_routes.py`
  + `__init__.py` — clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac. User to smoke-test against a real
  ComfyUI session (the previous URL-encoding 405 should be gone
  since the new route handlers own the URL space).

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

* feat(sidebar): redesign snapshot UX — single-click Save, real Settings panel

User feedback on #68's first cut: tabbed modal is too complicated, can't
see existing presets in the Save flow, the env var alone isn't a real
settings UI, the default-path text was stale (still claimed
`/userdata/koolook-presets/` after the configurable-path landed).

UX changes
----------
- **Save button → one click.** If a preset is currently loaded
  (tracked in localStorage as `koolook.snapshot.currentPresetName.v1`,
  survives reloads), prompts "Save over '<name>'?" with three options:
  Save (overwrite) / Save as new… (rename) / Cancel. If nothing is
  loaded yet, prompts for a name with `preset YYYY-MM-DD` default,
  fully editable.
- **Load → single dialog.** Preset list with per-row × Delete button.
  Header line shows the current library path so the user can see where
  they're loading from. No tabs.
- **Cog → real Settings panel.** Single field for the library path
  (text input, absolute path). Save / Reset to default / Close. Read-
  only line shows the currently-resolved path + source label
  (settings panel / env var / built-in default). The "Manage" tab and
  its Download / Upload sub-actions are gone — the user can already
  reach the file on disk after a save, no in-app file ops needed.

Server side
-----------
- New `GET /koolook/presets/settings` returns the saved library path +
  resolved path + source.
- New `POST /koolook/presets/settings` writes the saved library path
  to `<comfyui-userdata>/koolook-settings.json`. Empty value clears
  the override.
- Resolution chain in `_configured_dir()`:
    settings file > KFORGELABS_PRESETS env var > built-in default
  Reads each on every call; no module-load caching.
- `info` endpoint gains a `source` field ("settings" | "env" | "default")
  the UI uses for the readout line.

Client side
-----------
- `showSnapshotModal` (tabbed) replaced by three exports:
    showSaveSnapshotDialog, showLoadSnapshotDialog,
    showSnapshotSettingsDialog.
  Each is self-contained, no shared state.
- `snapshot.js` adds `getSettings`/`saveSettings` for the Settings
  dialog and `getCurrentPresetName`/`setCurrentPresetName` for the
  one-click Save tracking. Settings ride on the new server endpoints;
  current-preset tracking is browser-local.
- `applySnapshot` now also updates the current-preset tracker (via
  the dialog wrapper, not the function itself) on successful load.
- `tree.js` toolbar wires three direct callbacks instead of one
  `openSnapshotModal(initialTab)` helper.

Stale text fix
--------------
- The Save dialog's hint text in #68's first cut hardcoded
  "/userdata/koolook-presets/<name>.json" — stale from the userdata-
  era PR #67. The redesigned Save flow doesn't show that text at all
  (the path is no longer relevant to the user-typed name); the Load
  dialog's header line and the Settings dialog's read-only field are
  the canonical surfaces for "where my presets actually live."

Deliberately out of scope
-------------------------
- No per-save path picker. The library is one location at a time, set
  in advance via Settings or env var. Saving to ad-hoc locations would
  fragment the library and break the "facility shared library" use case.
- No in-app Download / Upload (was in #68's first cut). Files are on
  disk at the configured path; the OS file manager handles transfer.

Verification
------------
- `node --input-type=module --check` on every modified JS file —
  clean.
- `python -c "import ast; ast.parse(...)"` on `koolook_routes.py` —
  clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac. User to smoke-test against a real
  ComfyUI session.

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

* fix(sidebar): address PR #68 review — atomicity, security, error UX

Multi-agent review surfaced 2 BLOCKING + 5 HIGH + 4 MEDIUM + 3 LOW.
Validated the BLOCKINGs against the actual code (both real) and
applied the bundle of fixes that hold up. Modal-size HIGH (#7 file
size) deferred to a follow-up — that's a wider refactor.

BLOCKING fixes
--------------
- **`applySnapshot` partial-failure tracker gating** (modals.js Load
  dialog). `setCurrentPresetName(...)` was called unconditionally
  before checking `{picksOk, workflowsOk}` from the apply. Partial
  apply (one of picks/workflows persisted, the other didn't) left
  the tracker pointing at the preset, so the next Save would offer
  "Save over '<name>'?" and silently overwrite the on-disk preset
  with the corrupted half-state. Now: tracker is set only on
  `picksOk && workflowsOk`; partial failure clears it (and the toast
  tells the user "Reload to recover prior state. Tracker cleared.").

- **`replaceAllWorkflows` non-atomic rebind** (workflows_store.js).
  Rebinds `workflowsCache` before awaiting persist, with no rollback.
  Now: snapshots the cache via the existing `snapshotCache()`
  primitive that `persistMutation` uses; on persist failure, restore
  the prior cache and return false. Load is fully atomic.

HIGH fixes
----------
- **Workflows-store invariant block updated.** Lists `replaceAllWorkflows`
  as the fourth legitimate rebind site (with rollback semantics).
  "Fifth rebind" warning bumped to "sixth."
- **Explicit HEAD route on `/koolook/presets/file`** that just does
  `is_file()` + status, no body work. The auto-synthesis path was
  reading the full file from disk just to discard it — wasteful on
  multi-MB snapshots over Dropbox/iCloud/NFS.
- **Client `sanitizeName` mirrors server whitelist regex**
  (`^[A-Za-z0-9 _.()\-]+$`). Characters outside the whitelist (`+`,
  `&`, `'`, accented letters, emoji, etc.) collapse to `_` rather
  than triggering an opaque HTTP 400 at save time.
- **Error-reason surfacing via `await resp.text()`.** RFC 7540 strips
  HTTP/2 reason phrases; behind any HTTP/2-terminating proxy
  `resp.statusText` is empty. Reading the body always carries
  aiohttp's `reason=` text. Used by writePreset, deletePreset,
  saveSettings, readPreset.

MEDIUM fixes
------------
- **Symlink follow-through hardening.** `_resolve_within_base` helper
  computes `(base / name).resolve()` and asserts it's still under
  `base.resolve()`. Defends against a symlink planted in a facility-
  shared library dir redirecting reads/writes off-share.
- **Atomic settings write.** `_write_settings` writes to `<tmp>` then
  `os.replace`, so an interrupted process can't truncate the file
  and silently revert the user's saved library path on next load.
- **`__init__.py` import wrapped in try/except.** The "non-fatal"
  comment promised that an import failure wouldn't break node
  registration; previously only `install()` returning False was
  guarded. Now any module-load exception is caught and logged with
  the `[Koolook]` prefix, and the rest of the plugin loads.

LOW housekeeping
----------------
- Dropped dead exports `downloadSnapshotAsFile` + `readSnapshotFile`
  (left over from an earlier iteration that had Download/Upload UI).
- Dropped 5 dead CSS rules (`.koolook-snapshot-tabs*`,
  `.koolook-snapshot-tab-pane`) from when the modal was tabbed.
- `currentPresetName` cleared when the user deletes the currently-
  loaded preset (Load dialog's promptDelete now checks via the new
  `getCurrentPresetName` capability).
- `loadPreview` warns to console on `kind` mismatch — non-snapshot
  JSON files in the library directory are now visible in DevTools
  rather than silently filtered.
- `presetExists` is tri-state (`true` / `false` / `null`) — the
  Save flow refuses to write when the library is unreachable rather
  than silently skipping the overwrite-confirm prompt.

Deferred
--------
- modals.js size (1080 lines) — extracting the snapshot dialogs into
  their own file is a follow-up refactor. Not blocking merge.
- Doc drift on maintainer docs / README env-var convention — separate
  documentation sweep.

Verification
------------
- `python -c "import ast; ast.parse(...)"` on all .py files — clean.
- `node --input-type=module --check` on every modified JS file —
  clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- `grep -rn "downloadSnapshotAsFile\\|readSnapshotFile\\|koolook-snapshot-tab" web/`
  — zero hits (dead code gone).
- Synced to live install for browser smoke-test.

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

* ci: nudge — re-trigger CI after webhook miss on d6d2bf8

The CI workflow didn't fire on the prior push (Socket Security ran but
GitHub Actions Lint/Smoke/Pre-flight/Security Gate didn't). Empty
commit to refire the pull_request webhook.

* fix(routes): drop unnecessary f-string prefixes flagged by ruff (F541)

Two adjacent string literals in the symlink-escape error message had
an `f` prefix despite containing no placeholders. CI's ruff check
fails on F541 — strip the prefixes so the lint gate passes.

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 15:06:46 +02:00
malkuthroandClaude Opus 4.7 b89355ded1 feat(sidebar): one-click install of missing nodes via ComfyUI-Manager (#69)
Adds an "Install missing for picks" toolbar button to the Nodes section.
On click it walks the user's curated picks against Manager's
/customnode/getmappings, buckets them into already-installed /
will-install / unresolved, queues each unique git URL through
/customnode/install/git_url, polls /manager/queue/status to drive a
progress bar, and prompts to reboot — collapsing what was previously
a per-node "open node, let Manager prompt, click install, restart"
loop into a single confirm-and-go flow.

New `web/sidebar/installer.js` holds the Manager-API client (detect /
mappings / queue / poll / reboot) plus the picks → installs resolver.
The modal in `web/sidebar/modals.js` drives the four-phase UI
(discovery → confirm → progress → result) and surfaces 403s as
actionable language ("your security level forbids git-URL installs")
rather than raw HTTP. Falls back to a clipboard URL list (with a
`comfy node install` hint) if Manager isn't reachable.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:21:07 +02:00
malkuthro 8ebe0af210 Revert "fix(sidebar): avoid 409 on native save when Koolook workflow name collides with an existing native file (#65)" (#66)
This reverts commit 13de73a40a.
2026-05-05 09:43:33 +02:00
malkuthroandClaude Opus 4.7 13de73a40a fix(sidebar): avoid 409 on native save when Koolook workflow name collides with an existing native file (#65)
Symptom
-------
Loading a workflow from the Kforge Labs sidebar whose name matched an
existing native ComfyUI workflow file (e.g. user has both
`user/default/workflows/Hi-Res-FIX_v01.json` AND a Koolook entry
named `Hi-Res-FIX_v01`) and then clicking ComfyUI's native save
button produced a confusing toast:

  Error storing user data file 'workflows/Hi-Res-FIX_v01.json':
  409 Conflict

The user's only workaround was to rename via Save As.

Root cause
----------
ComfyUI's frontend save flow at
`Comfy-Org/ComfyUI_frontend/src/stores/userFileStore.ts`:

    const resp = await api.storeUserData(this.path, this.content, {
      overwrite: this.isPersisted,
      ...
    })

`overwrite: this.isPersisted` — passes `overwrite=true` only when
ComfyUI considers the workflow already persisted to disk. Workflows
loaded via the 4th-arg trick on `loadGraphData(..., wfName, {})`
become temporary workflows (`isPersisted = false`), so save POSTs
with `overwrite=false`. The server (correctly) 409s because a real
file already exists at `workflows/<wfName>.json`.

The 409 is **correct ComfyUI behavior** — protecting the user from
overwriting an unrelated file with the same name. ComfyUI's frontend
has no public API for extensions to mark a workflow as
"persisted-from-elsewhere," so we can't make their save flow use
overwrite=true for our loaded workflows.

Fix
---
`loadWorkflowOntoCanvas` now HEADs
`/userdata/workflows/<wfName>.json` before calling `loadGraphData`.
- No collision → tab name is the bare `<wfName>` (existing behavior).
- Collision → tab name becomes `<wfName> (Koolook)`. Save-back
  writes `workflows/<wfName> (Koolook).json`, which is a clean
  new artifact that doesn't collide with the user's native file.

The load toast surfaces the renaming when it happens, so the user
knows their tab was suffixed and why. When the deployment doesn't
support HEAD (rare), the catch falls through to the bare name —
worst case is the same 409 the user already knows how to work
around.

Verification
------------
- `node --input-type=module --check` on `canvas_io.js` — clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac via `scripts/sync_to_dev.py` for
  smoke-test against an actual ComfyUI session.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:34:26 +02:00
malkuthroandClaude Opus 4.7 2f370a962c chore(sidebar): finish #47 tidiness — UX touches + magic-string lift + small refactors (#64)
Closes the remaining checkboxes on #47 (Sidebar follow-ups deferred
from PR #45). Bundles seven items in one PR per the new "default to
bundling small slices" policy: two user-visible UX improvements + five
internal hygiene fixes.

User-visible (Changed)
----------------------
- **Save selection toast differentiates "no selection" from "selection
  points at deleted nodes."** `serializeSelection` returns a
  discriminated `{ kind: "empty" | "stale" | "ok", graph? }` instead
  of plain `null` / graph. The "stale" case (selection set non-empty
  but every id refers to a removed node — common after undo/redo)
  now toasts "Selected node(s) no longer exist. Click a node on the
  canvas to re-select." instead of the misleading generic message.
- **Curated-defaults seed failure now toasts.** Previously
  console-only; users without DevTools never saw it. The most common
  cause (localStorage quota / private-mode rejection) is something
  the user can act on.

Internal (no behavior change)
-----------------------------
- Lifted save-modal action sentinels (`new` / `use_existing` /
  `modify_existing`) into `ACTION_*` named constants at the top of
  `showSaveWorkflowModal`, alongside the pre-existing `NEW_TOP` /
  `SAVE_HERE` cascade sentinels.
- Section-id constants (`SECTION_ID_NODES`, `SECTION_ID_WORKFLOWS`,
  `SECTION_ID_TAGS`) lifted to module scope in `tree.js`. `pinExpanded`
  in the save flow now references the constant instead of the literal
  `"workflows"` string, so a future section-id rename only needs the
  constant updated.
- `modalLabel(text)` factory in `modals.js` replaces 7× repetition of
  the four-line `createElement("label")` + `className` + `textContent`
  + `appendChild` pattern.
- `USERDATA_OVERWRITE_QUERY = "?overwrite=true"` named constant in
  `workflows_store.js`. ComfyUI's userdata API requires this flag to
  allow POST over an existing file; pinning the contract as a
  constant makes it visible at a glance.
- `buildFolder` no longer round-trips folder-expansion state through
  `wrapper.dataset.expanded`. State lives in a closure variable;
  `pathStates` remains the canonical cross-render store.
- `subcategoryFor` fallback path now logs `console.warn` with the
  offending category + `categoryRoot`. Reachable only via a logic
  error (stale `REPOS` config); previously silent.
- Save-modal `onSave` callback contract renamed `dir` → `dirPath` to
  match codebase-wide convention (`dirPath` for arrays of segments,
  `dirName` for single segments, `dir` for resolved DirNode objects).
  Caller updates in `tree.js` paired.

Skipped from #47 (with rationale)
---------------------------------
- The broader `el(tag, props, children)` DOM helper from the issue
  body. Adopting it consistently would touch every modal/folder/leaf
  row construction site (~40+ replacements) for marginal readability
  win. The narrower `modalLabel(text)` is the meaningful slice
  because it replaces an exact 4-line repetition. If `el()` becomes
  desirable later, file as a focused refactor PR.

Verification
------------
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- `node --input-type=module --check` on every modified JS file —
  clean.
- Synced to live ComfyUI install via `scripts/sync_to_dev.py` for
  smoke-test on Mac.

Closes #47.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:19:48 +02:00
malkuthroandClaude Opus 4.7 bebc6f618d feat(sidebar): right-click menu pack — Duplicate, Tags, + New directory move (#59)
* feat(sidebar): right-click menu pack — Duplicate, Tags, + New directory move

Three sibling sidebar slices bundled because they all extend the same
workflow-row right-click menu in `web/sidebar/tree.js` and share a small
schema/store surface in `workflows_store.js`. Refactor cleanup is held
back for a separate PR (issue #47) so this diff stays a "no behavior
change to existing flows" feature add.

Changes
-------
- Right-click "Duplicate…" → name modal with `<name> (copy)` default;
  deep-clones the source graph into a new entry in the same directory
  via `saveWorkflowEntry`. The duplicate inherits the source's tags so
  the user's categorization carries over. Same-name duplicates fall
  through to the existing archive-on-collision behavior. Closes #58.
- Right-click "Tags…" → chip-row modal with add/remove. Each edit
  fires its own `persistMutation` so changes survive a mid-edit
  close. New `showTagsModal` in `modals.js` mirrors the existing
  `showInputModal` / `showConfirmModal` shape (escape-key teardown,
  overlay-click dismissal centralized in `makeModalShell`).
- New Tags sidebar section between Workflows. Flat list of tag folders;
  each tag holds the workflows carrying it (sorted A→Z). Click loads
  the workflow from its real path. Archived workflows are filtered
  out of the section so the active view stays clean — their tags
  persist on the entry, so a restore brings them back. Search matches
  tag name OR workflow name.
- Right-click "+ New directory…" / "+ New subdirectory under <path>…"
  in the Move-to flow. Both create a fresh directory and move the
  workflow into it as one atomic mutation; if the move can't land,
  the new directory is rolled back so the cache never leaks an
  empty orphan. Closes #57.

Schema additions (back-compatible)
----------------------------------
- Workflow entries gain optional `tags: string[]`. `normalizeDirNode`
  trims, drops empties, and dedupes case-sensitively, so old entries
  without a tags field load as `tags: []` and the rest of the code can
  assume the field always exists. Pre-v0.3 stores load fine.
- New exports in `workflows_store.js`: `getWorkflowTags`, `addTag`,
  `removeTag`. `getWorkflowGraph` is now also exported so the duplicate
  flow can deep-clone without reaching into the cache directly.

Why one PR for three issues
---------------------------
All three slices touch `workflowRowContextMenu` in `tree.js` and the
mutator API in `workflows_store.js`. A coherent UX-feature scope ("next
batch of workflow menu enhancements") reviews more cheaply as one
diff than three near-identical PRs. Issue #47 (refactor / tidiness
pass) stays separate because mixing refactor and feature in the same
diff defeats the point of the split — that work is queued for a
follow-up PR.

Testing
-------
- `python tools/preflight_release.py --skip manager-meta -v` PASS
  (static / dispatch / workflows fixtures, all 3 stages).
- `node --input-type=module --check` passes on each modified JS file.
- Browser smoke test: pending — needs a ComfyUI session for the
  user to verify the three menu actions and the Tags section against
  a real workflow store. The CI gates are Python-only and don't catch
  JS regressions in the sidebar by themselves.

Related
-------
- Closes #57 (move-to: + New directory…)
- Closes #58 (Duplicate)
- Implements the right-click + section parts of #56 (Tags); the issue
  body's `addTag` / `removeTag` / `listAllTags()` ops landed under
  slightly different names (`addTag`, `removeTag`, `gatherTags`).

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

* fix(sidebar): address review findings on PR #59

Self-review pass against the validated agent findings. Five fixes total
across the three issues' surface, holding refactor/file-size concerns for
follow-up #47 widening.

Fixes
-----
- **HIGH** `normalizeDirNode` now rejects non-string `tags` elements
  outright instead of `String(raw).trim()` coercing them. `null` /
  `undefined` / `true` / `123` no longer silently surface as the literal
  tag strings `"null"` / `"undefined"` / etc. — `stats.dropped`
  increments instead so the existing console warning catches malformed
  migrations. Closes review HIGH-1.
- **HIGH** Tags modal now distinguishes "no tags yet" from "workflow
  no longer exists." The `getCurrentTags` adapter no longer collapses
  `null` to `[]`, and `renderChips` renders an explicit gone-state
  with disabled add input + descriptive placeholder. Prevents users
  typing into a doomed modal when a concurrent action (another tab,
  drag-and-drop) deletes the workflow under them. Closes review HIGH-3.
- **MEDIUM** Duplicate flow re-validates source existence at submit-time
  inside the `mutate` callback. Without it, a concurrent delete of the
  source between menu-open and submit would let `saveWorkflowEntry`'s
  `ensureDirectoryAtPath` silently re-create the parent directory and
  paste a clone of the captured graph — effectively undoing someone
  else's delete. Now returns `false` on missing source, surfacing
  "Could not duplicate — <name> no longer exists." Closes review
  MEDIUM-6.
- **MEDIUM** Duplicate flow now routes inherited tags through the
  public `addTag` mutator instead of writing `dir.workflows[newName].tags`
  directly. Keeps the tree.js UI layer outside the store's internal
  shape. The whole sequence still rides on one `persistMutation`
  snapshot — commit failure rolls back save + tag adds together.
  Closes review MEDIUM-5.
- **LOW** `gatherTags` comment block clarifies that grouping is
  case-sensitive but search filter is case-insensitive substring.
  The comment previously said "Comparison is case-sensitive" which
  was technically true for grouping but misleading on its own.

Skipped with rationale
----------------------
- **HIGH-2 modal cache desync on persist failure** — agent analysis
  was wrong on trace. `renderChips` reads from cache, X-click does
  not optimistically remove the chip from DOM, and `restore()`
  reverts cache to pre-mutation. DOM and cache stay consistent across
  every commit-failure path. Cross-tab updates aren't reflected, but
  that's a project-wide cache-coherency concern, not specific to this
  PR.
- **HIGH-4 tree.js size** — real architectural concern but follow-up
  only. Track on #47 widening (or a sibling extraction issue).
  Adding extraction in this PR would mix refactor with feature.
- **MEDIUM-7 tag API asymmetry** — agent claimed `addTag`'s parameter
  reassignment differed from neighbors, but verified the existing
  convention IS parameter reassignment (`addDirectory`, `renameDirectory`,
  `renameWorkflow`, `moveDirectory` all do `name = (name || "").trim();`).
  The defensive-copy difference between `getWorkflowTags` and
  `getWorkflowGraph` is intentional.
- **MEDIUM-8 top-level "Archive" name** — pre-existing semantic shared
  by toolbar Create-directory and save-modal "+ New directory."
  Restricting only the right-click new-dir entry would be inconsistent.
  Filing as a separate cross-cutting issue is the cleaner path.
- **LOW magic strings / modals.js size** — both queued for #47.

Validation
----------
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- `node --input-type=module --check` on all three modified JS files — clean.

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 08:59:13 +02:00
malkuthroandClaude Opus 4.7 8c1e6e3d78 chore(sidebar): tidiness pass — toolbar factory, dropped dead code, store invariants (#60)
* chore(sidebar): tidiness pass — toolbar factory, dropped dead code, store invariants

Refactor pass against four pre-existing items on #47. No behavior change;
the diff is "extract helper + drop dead code + add comments" only. Held
back from PR #59 so feature and refactor stay isolated for review.

Changes
-------
- **`makeToolbarButton({iconClass, title, onClick})`** factory replaces
  the four near-identical button-construction blocks in `renderPanel`
  (export, new-dir, save-canvas, save-selection). Each block was 5–7
  lines of `createElement` + class/innerHTML/title/listener wiring;
  call sites collapse to 4-line factory invocations.
- **Dropped duplicate `.koolook-export-btn` CSS rule.** It was
  byte-for-byte identical to `.koolook-icon-btn` (minus the latter's
  `:disabled` styling, which the export button never hit). The export
  button now uses only `.koolook-icon-btn` via `makeToolbarButton`.
- **`buildFolder({path})` default removed.** Previously
  `path = null` with an `if (path && …)` runtime guard. Verified all
  callers (both inside `renderTree` and inside `makeSectionCtx`) always
  supply a non-empty string — the default was never reachable. Param
  is now required; runtime guards are dropped.
- **`workflowsCache` mutator invariants documented inline.** Four
  rules a future contributor needs before adding a new mutator:
  - Pair every mutate with a commit (or use `persistMutation`) — a
    mutator that "succeeds" without a paired commit silently drops the
    change on the next reload. We've already shipped + reverted that
    class of bug once; the comment names it explicitly.
  - Return `false` for no-op (collisions, missing source, validation
    failures), truthy for success. `persistMutation` reads this.
  - Mutate in place; don't return new structures.
  - Never replace `workflowsCache` itself except in seed/load paths.

Closes
------
Four checkboxes from #47 (architectural + code-tidiness):
- "Decompose `renderPanel` + extract `makeToolbarButton`"
- "Drop duplicate `.koolook-export-btn` CSS class"
- "`buildFolder` `path = null` default (unreachable) — drop"
- "Document `workflowsCache` invariants"

Remaining #47 items not in this PR (queued for a future widening):
- DOM helpers `el(tag, props, children)` + `modalLabel(text)` — wider
  refactor that touches modal/folder/leaf row construction.
- Magic-string lift (action sentinels in save modal, `?overwrite=true`,
  `pathStates` mini-DSL).
- `dir` / `dirName` / `directory` terminology pass.
- `dataset.expanded` boolean storage instead of string compare.
- UX items (`subcategoryFor` fallback, seed-failure toast, selection
  serializer stale-id case).

Validation
----------
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- `node --input-type=module --check` on each modified JS file — clean.
- Pure refactor: `git diff` is mechanical extraction + dead-code drop +
  comment additions. No semantic changes to existing flows; no new
  user-visible state.

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

* fix(sidebar): address PR #60 review findings

Multi-agent review surfaced 2 MEDIUM + 3 LOW. Fixed 4 of the 5; skipped
the JSDoc-on-makeToolbarButton LOW because sibling factories (makeFolderRow,
makeNodeLeafRow, makeWorkflowLeafRow) also have no JSDoc — adding it just
on the new factory would be inconsistent local style.

Fixes
-----
- **MEDIUM** `makeToolbarButton` no longer uses `innerHTML` template
  interpolation. Icon span is built with `createElement` + `className` +
  `appendChild` instead. All current callers pass static literals, so this
  isn't a fix for an active vulnerability — it's helper-boundary hardening
  so a future caller passing dynamic data (workflow metadata, registry
  entry, etc.) can't inject markup. Also removes the file's only
  `innerHTML`-with-interpolation outside the empty-string clear at L157,
  matching the rest of the file's DOM-API style.
- **MEDIUM** Mutator-invariant rule #4 in workflows_store.js corrected to
  list all three legitimate `workflowsCache` rebind sites: module init,
  load/seed paths, AND `snapshotCache().restore()` during rollback.
  Previous wording denied that rollback rebinds, which would have led a
  future contributor reading the doc to either "fix" snapshotCache
  (breaking rollback) or distrust the entire invariant block.
- **LOW** `buildFolder({path})` now throws on `typeof path !== "string" ||
  !path` at function entry. Converts a silent state-corruption regression
  ("future caller forgets to pass path → `pathStates.set(undefined, …)`
  pollutes one shared key across all unkeyed folders") into a loud
  boundary failure. Promotes the PR description's "unreachable default"
  claim into a self-enforcing assertion.
- **LOW** `makeToolbarButton` doc comment tightened from "every toolbar
  button" to "the four icon-only toolbar buttons (export, new-dir,
  save-canvas, save-selection)" with explicit acknowledgment that the
  text-content `addBtn` (Add-to-favorites "+") stays hand-rolled.

Skipped (with rationale)
------------------------
- **LOW** Add JSDoc to `makeToolbarButton` — sibling factories
  `makeFolderRow`, `makeNodeLeafRow`, `makeWorkflowLeafRow` have no JSDoc
  either. Adding it just on the new factory would be inconsistent. If the
  whole DOM-helpers section gets JSDoc later (under #47's wider widening),
  this factory should be in scope at the same time.

Validation
----------
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- `node --input-type=module --check` on each modified JS file — clean.
- `grep -n "innerHTML" web/sidebar/tree.js` → only the L157 empty-string
  clear remains.
- `grep -n "workflowsCache *=" web/sidebar/workflows_store.js` → all
  rebind sites (init, load×5, rollback, seed) covered by the corrected
  rule #4.

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 08:58:38 +02:00
malkuthroandClaude Opus 4.7 86a3edaf9e fix(sidebar): place inserted nodes at visible center on HiDPI displays (#62)
`placeAtCanvasCenter` in canvas_io.js was using `canvas.canvas.width` /
`.height` to compute the canvas-center coordinates for newly inserted
nodes. Those properties report the WebGL-style backing-store pixel
buffer dimensions, which on a HiDPI display (Retina/Mac, Windows DPI
scaling > 100%) are `devicePixelRatio` × the visible CSS-pixel
dimensions. LiteGraph's pan/zoom math operates in CSS pixels, so
halving the buffer width put the node at devicePixelRatio× the
visible-center distance from the graph origin — well off the right
edge of the viewport.

Symptom on Mac (Retina, devicePixelRatio = 2): clicking a node in
the Kforge Labs sidebar created the node, but it landed roughly off
the right edge of the visible canvas. The minimap showed it
appearing far from the user's current view; the canvas-center was
unaffected. Worse on zoomed-in / panned canvases.

Why it worked on Windows: most Windows desktops run at 100% DPI
(devicePixelRatio = 1), where buffer pixels and CSS pixels are
identical and the original math is correct.

Fix: use `canvas.canvas.clientWidth` / `.clientHeight` (CSS pixel
dimensions). Falls back to `.width` / `.height` if the canvas hasn't
been laid out yet (during early init), preserving the prior behavior
in that edge case.

No other call sites in the codebase use the same backing-store-pixel
pattern (verified via `grep -rn "canvas\.canvas\.width"` in web/).
The bug was localized to this one helper.

Verification
------------
- `node --input-type=module --check` on canvas_io.js — clean.
- Live test on Mac (synced via `scripts/sync_to_dev.py` to a real
  ComfyUI install): clicking a sidebar node row now creates the node
  near the canvas center across multiple zoom levels and pan offsets.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:58:12 +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 a609e75d40 refactor(web): tree pipeline as section-registry with two-tier API (#52)
* refactor(web): tree pipeline as section-registry with two-tier API

Implements issue #50. Behavior-preserving — same DOM, same path-state
keys, same render cadence, same context menus. The new engine deepens
the tree module: a 3-function public surface hides the section
iteration, divider insertion, forceExpanded threading, path prefixing,
empty-state placeholders, and pathStates lifecycle.

Public surface (3 functions, all module-private — only renderPanel is
exported):
  - addSection(spec)        — register a section descriptor
  - renderTree({treeEl,query}) — single render entry point
  - pinExpanded(paths)      — pin folders open for the next render

Internals:
  - SECTIONS array (declaration order = render order)
  - pinnedPaths Set (consumed every render)
  - makeSectionCtx → builds ctx.folder({...}) / ctx.leaf({row}) with
    automatic path prefixing (sections write segment-relative paths;
    engine produces ${section.id}/<seg>/...)
  - validPaths Set populated as folders render; pathStates pruned at
    end of each render against validPaths ∪ pinnedPaths — fixes the
    unbounded-growth bug from renames/deletes leaving stale keys

Built-in sections:
  - "nodes": curated-nodes section, merges gatherNodesByRepo +
    gatherUserPickPacks into one displayed group with auto-pack and
    user-pick subgroups (same UX as before)
  - "workflows": directories with Archive sub-folders and right-click
    context menus

Save flow:
  - saveAndToast now calls pinExpanded(["workflows", "workflows/${dir}"])
    before persistMutation; the persist's notifyWorkflowsChanged fires
    a re-render that consumes the pin and opens the destination folder

Adding a new section is now ~12 lines (simple flat list) or ~30 lines
(advanced tree with row interactions) — the engine handles cross-cutting
concerns. Foundation for tags / recents / pinned / templates features.

Closes #50.

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

* fix(sidebar): scope pathStates prune to rendered sections + always apply pins

Two HIGH issues from the multi-agent review on PR #52:

1) Pruning was over-broad. When section A is gated empty by a search
   filter, its (legitimate) pathStates keys were getting nuked because
   they weren't in `validPaths` — clearing the search reverted the
   user's prior expansions to startExpanded defaults. Fix: track which
   sections actually rendered this pass (`renderedSectionIds`); prune
   only keys whose owning section rendered.

2) `pinnedPaths` could leak across renders when the cross-section
   search-empty branch took an early return without consuming the pin
   set. A pin from one save would later be applied to an unrelated
   render. Fix: drop the early return; phase 3 (prune + pin apply) now
   always runs.

Also tightens code-quality LOW findings:
- `let pinnedPaths` -> `const pinnedPaths` + `.clear()` after snapshot
  via `new Set(pinnedPaths)`.
- Add `@typedef SectionCtx` and `FolderOpts` JSDoc blocks (referenced
  by `SectionSpec.build` but missing in the initial commit).

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 18:12:44 +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 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
malkuthro 6661a392a4 Add dissable VERSIONING+enable overwrite 2025-10-26 18:32:37 +01:00
malkuthro 414b6b19ca External path FIX 2025-10-26 18:03:49 +01:00
malkuthro a95531ccaa Get info - deleted 2025-10-18 23:22:03 +02:00
malkuthro 682804c155 Get info -A 2025-10-18 22:08:10 +02:00
malkuthro 58f79c11e9 make preDefined pad and panel colors 2025-10-18 21:40:40 +02:00
malkuthro 85ee519631 FIN - working - 2 buttons 2025-10-05 00:21:40 +02:00
malkuthro d85d9c9420 base dir - try-A 2025-10-04 22:58:56 +02:00
malkuthro 0303343d06 WORKING button 2025-10-04 22:36:53 +02:00
malkuthro ccd091be82 adding buttons - A 2025-10-04 22:12:40 +02:00