2f370a962c0bb2845c254d35d4c64f2dba5ceacf
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
6661a392a4 | Add dissable VERSIONING+enable overwrite | ||
|
|
414b6b19ca | External path FIX | ||
|
|
a95531ccaa | Get info - deleted | ||
|
|
682804c155 | Get info -A | ||
|
|
58f79c11e9 | make preDefined pad and panel colors | ||
|
|
85ee519631 | FIN - working - 2 buttons | ||
|
|
d85d9c9420 | base dir - try-A | ||
|
|
0303343d06 | WORKING button | ||
|
|
ccd091be82 | adding buttons - A |