Files
malkuthro-ComfyUI-Koolook/docs/maintainers/workflows-sidebar.md
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

9.6 KiB

Workflows sidebar — quick reference

The "Workflows" section of the Kforge Labs sidebar tab; implementation lives in web/sidebar/, with the workflow store + ops in workflows_store.js and the tree rendering in tree.js.

Save / load loop

  1. Click 💾 (whole canvas) or ✂ (selection only) in the Workflows action bar.
  2. Modal: Directory (flat path picker) → Base on existing (optional) → Action → Workflow name (shown only when needed).
  3. Saved entry appears at the bottom of its directory in the tree.
  4. Click any workflow row to load it onto a fresh tab; the tab title takes the workflow's name.

The directory dropdown lists every directory path in the tree as a flat list (UP-scale, UP-scale / Type-A, Depth, …). The "+ New directory…" option creates a new top-level directory. Subdirectories are created via right-click on an existing directory (see below).

Action semantics

Action Resulting name Archive behavior
New name typed auto-archives only if the typed name happens to collide
Use existing name (archive previous) base always archives the existing entry
Modify existing name base, edited no archive (different name)

The archive trigger is a name collision with any existing active entry, regardless of which action was selected. The Action dropdown is UX guidance; the underlying logic flows from the resulting (name, dir) pair.

Modules — splice a saved cluster into your live canvas

A saved workflow tagged with the literal tag module behaves like a reusable building block: clicking it inserts its nodes into the existing canvas (placed at the viewport center, freshly id-remapped, internal links re-created, the inserted nodes left selected) instead of replacing the graph the way a normal Load does.

Typical use cases this is designed for:

  • EXR output stack — a Save EXR + path-builder + format selector cluster the user wants to drop into any render-time workflow.
  • Depth-only render — a depth-map node + preview wired up, ready to be spliced into a wider pipeline.
  • Wan 2.2 prompt module — the EasyWan22Prompt node + a CLIP encoder
    • any conditioning glue, saved once, dropped into ten different workflows.

How it works:

Surface Behavior
Save modal (selection save) Includes a Save as module checkbox, pre-checked for selection saves. Whole-canvas saves get the same checkbox but unchecked by default. The tag is added in the same atomic persistMutation as the save — a commit failure rolls both back.
Workflows section Module-tagged entries get a green pi pi-plus-circle icon and a distinct hover tooltip. Left-click inserts instead of loading. Right-click still offers both Load and Insert into canvas — Load stays available for every row regardless of tag.
Tags section Same flip — a workflow tagged module is treated as a module no matter which tag folder it shows up under.
Archive folder Archived module entries still left-click to Load (they're old versions, treated as "review" rather than "splice"). Right-click still has both Load and Insert.

Implementation pieces:

  • The literal tag string lives in web/sidebar/constants.js as MODULE_TAG.
  • The non-destructive insert primitive is insertWorkflowOntoCanvas(dirPath, wfName) in web/sidebar/canvas_io.js. It pre-flights every referenced node type against LiteGraph.registered_node_types, aborts cleanly with a toast when any are missing (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't 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-of-cluster placement uses CSS pixels (clientWidth/clientHeight), not the HiDPI backing buffer — same correction as the existing placeAtCanvasCenter helper.

To turn an existing saved workflow into a module: right-click → Tags… → add module. (No re-save needed; the row re-renders on the next workflows-changed event.) To un-module: remove the module tag the same way.

Right-click menus

  • Workflow row: Load / Insert into canvas / Rename / → <other path> (lists every other directory in the tree) / Move to archive (or Restore from archive if already archived) / Delete.
  • Directory row (any depth): Create subdirectory… / Rename / Delete (with confirm if non-empty — the message names workflow + subdirectory counts).
  • Archive folder (synthetic — appears only when a directory has archived workflows): Delete archive (N) — removes every archived workflow in this directory in one go (active workflows in the same directory are untouched).

Drag-and-drop (Tier 1 — moves only; reordering is alphabetical)

Drag Drop on Effect
Workflow row Directory row Move workflow to that directory (no-op if same dir)
Workflow row Archive folder Archive in that directory (move first if cross-dir)
Directory row Directory row Nest the dragged dir as a child of the target

Cycle prevention: dragging a directory onto itself, its current parent, or any of its descendants is rejected. Name collisions (the destination already has a sibling with the same name) are rejected. Reserved name: dropping a directory literally named Archive into a non-root parent is rejected (would shadow the synthetic Archive folder).

Visual feedback: drop targets get a blue outline on hover. Failed drops surface a toast.

Sort within a level remains alphabetical — Tier 1 doesn't introduce a custom-order schema. (A future Tier 2 could.)

Subdirectories

Every directory can host nested subdirectories at arbitrary depth. Each nested directory behaves like a top-level directory: it can hold workflows, its own Archive subfolder (when any of its workflows are archived), and further subdirectories.

  • Create: right-click a directory → "Create subdirectory…"
  • Save into: pick the nested path from the save modal's directory dropdown (UP-scale / Type-A)
  • Move across: right-click a workflow → choose any other path in the move submenu
  • Reserved name: subdirectory names cannot be Archive (case-insensitive) — that string is reserved for the synthetic Archive folder rendered for archived workflows. Top-level directories named Archive are allowed (no collision at root).

Storage

Where What
/userdata/koolook_workflows.json (ComfyUI install) Primary. Per-install.
localStorage["koolook.workflows.fallback.v1"] Used only if /userdata is unreachable.
localStorage["koolook.workflows.seeded.v1"] "1" once defaults have been seeded for this install.
web/workflow_defaults.json (in repo) Distributed starter pack — seeded once into /userdata on first load with empty workflow data.

JSON shape (recursive — directories lives both at the root AND inside every directory node):

{
  "directories": {
    "<dir>": {
      "workflows": {
        "<name>": {
          "savedAt": "<ISO timestamp>",
          "graph": { /* serialized ComfyUI workflow JSON */ },
          "archived": true
        }
      },
      "directories": {
        "<sub>": {
          "workflows": { /* … */ },
          "directories": { /* recurses */ }
        }
      }
    }
  }
}

archived is optional (false/missing means active). Pre-v0.3 stores without the nested directories field still load fine — normalize treats a missing directories as {} and the rest of the code assumes it always exists after normalization.

Reset (force re-seed from defaults)

Delete the userdata file in the live ComfyUI install:

rm <ComfyUI>/user/default/userdata/koolook_workflows.json

Then in the ComfyUI page DevTools console:

localStorage.removeItem("koolook.workflows.seeded.v1");
localStorage.removeItem("koolook.workflows.fallback.v1");
location.reload();

Things that surprised us — keep in mind

  • Selection saves are partial graphs. Only the selected nodes + links between them survive; links into/out of non-selected nodes are nulled out, so the loaded workflow never has dangling references.
  • Workflow tab name comes from app.loadGraphData(graph, true, true, name, {})'s 4th arg. The tab flips from "Unsaved Workflow (N)" to the saved name, and Ctrl+S pre-fills with that name. (Verified against Comfy-Org/ComfyUI_frontend src/scripts/app.ts.)
  • Folder expansion state persists across re-renders (the pathStates Map in web/sidebar/tree.js). Saving never collapses the directory you were viewing — and a save into a nested path opens every ancestor folder.
  • Archive sub-folder is rendered above active workflows in each directory (and below nested subdirectories), so the latest active workflow sits closest to the bottom of its directory — easy to spot.
  • Directory header counts include all descendants. A parent dir with no direct workflows but multiple subdirectories shows the recursive total.
  • No auto-pruning of archives. Re-saving the same name many times leaves many timestamped archive entries. Right-click → Delete to remove individual ones when no longer useful.
  • Same-pattern distribution as curated_defaults.json — see curated-sidebar.md for the seeding semantics that also apply here.