Five user-visible changes to the Kforge Labs sidebar that complete the
dev-sync verification loop and fix theme-mode discovery:
- Build-tag footer at the bottom of the sidebar: `dev <sha> · <time>`
on line 1 (SHA at 13px monospace, the at-a-glance identifier),
italic <scope> on line 2. Reads `web/_dev_build.json` written by
`scripts/sync_to_dev.py --scope "<short scope>"`. Visible-in-app
companion to PR #95's chat-report convention — closes the gap when
the maintainer is juggling parallel sessions and can't tell whether
the browser tab is showing the latest sync. Absent on registry
installs; footer renders empty there.
- Theme/category mode now mirrors repo-mode population: REPOS auto-
pull union user-picks, deduped, with the new auto-pull-hidden set
filtered out. The earlier picks-only design dropped the entire
Koolook pack from theme mode on a stock install, even though those
same nodes appeared in repo mode — silently violating the "my
favorites in two views" mental model. `gatherNodesByTheme` rewritten
to seed from REPOS first, then user picks, with the same
autoCategoryRoots + hidden filtering repo mode uses.
- `×` works on every favorite, including auto-pulled REPOS nodes.
Adds `koolook.autoPullHidden.v1` localStorage Set; `×` on an auto-
pulled row calls `hideAutoPullType(type)` so the next render skips
it. `addToMyPicks` calls `unhideAutoPullType` as a side effect so
re-adding via `+` automatically restores visibility. Same uniform
"remove / bring back" UX whether the node was a user pick or REPOS
auto-pull.
- Pack-name badges restored on theme-mode leaves only. Small dim
`Koolook` / `KJNodes` text on the right of each row, same opacity
as the search-flatten breadcrumb. Memory aid for "where did this
node come from?" Repo mode and search-flatten still skip the badge
(origin is conveyed by tree nesting / breadcrumb prefix).
- `scripts/sync_to_dev.py --scope` flag: extends PR #95's brief-
output design with a JSON artifact (`<target>/web/_dev_build.json`)
carrying commit + timestamp + worktree + scope, which the sidebar
footer fetches on render. Doesn't change the existing one-line
chat-report contract or the `_build_line()` shape.
Tested via dev-sync against the live ComfyUI install:
- Footer renders correctly (`dev afe539e · 2026-05-06 15:38` + scope)
- Image bucket in theme mode now contains Koolook image nodes
- Pack badges visible on theme leaves
- × hides auto-pulled Koolook nodes; toolbar + restores them
CHANGELOG entries added; the prior conflicting [Unreleased] notes
about picks-only theme + pack-badge removal are flagged as superseded
in the new entries (release prep can squash).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
4.2 KiB
JavaScript
116 lines
4.2 KiB
JavaScript
// SPDX-FileCopyrightText: 2025-2026 Kforge Labs <https://github.com/malkuthro/ComfyUI-Koolook>
|
||
// SPDX-License-Identifier: GPL-3.0-only
|
||
|
||
// =============================================================================
|
||
// User picks (localStorage, browser-local persistence)
|
||
// =============================================================================
|
||
import {
|
||
STORAGE_KEY,
|
||
PICKS_CHANGED_EVENT,
|
||
AUTOPULL_HIDDEN_KEY,
|
||
} from "./constants.js";
|
||
|
||
export function loadUserPicks() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (!raw) return [];
|
||
const parsed = JSON.parse(raw);
|
||
return Array.isArray(parsed) ? parsed.filter(x => typeof x === "string") : [];
|
||
} catch (e) {
|
||
// Surface corruption so the user has a chance to spot it in the console
|
||
// before the next saveUserPicks() overwrites the bad blob with a fresh list.
|
||
console.warn("[Koolook] failed to parse user picks; returning empty list:", e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function saveUserPicks(picks) {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(picks));
|
||
return true;
|
||
} catch (e) {
|
||
console.warn("[Koolook] failed to save picks:", e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Tri-state: "added" | "duplicate" | "failed". Callers need to distinguish
|
||
// "save failed" from "already in list" so the user isn't told a write
|
||
// succeeded when localStorage rejected it (quota / private mode).
|
||
export function addToMyPicks(typeName) {
|
||
if (!typeName) return "failed";
|
||
// The `+` action always means "make this visible in favorites." If the
|
||
// user previously hid this type via `×` (when it was auto-pulled by a
|
||
// REPOS{select: "all"} entry), clear that hide as a side effect — so a
|
||
// subsequent gather pass surfaces it again either via picks or via auto-
|
||
// pull. Without this, `×` then `+` on the same Koolook node would do
|
||
// nothing visible — the hide would silently outlast the re-add.
|
||
unhideAutoPullType(typeName);
|
||
const picks = loadUserPicks();
|
||
if (picks.includes(typeName)) return "duplicate";
|
||
picks.push(typeName);
|
||
return saveUserPicks(picks) ? "added" : "failed";
|
||
}
|
||
|
||
export function removeFromMyPicks(typeName) {
|
||
saveUserPicks(loadUserPicks().filter(p => p !== typeName));
|
||
}
|
||
|
||
// =============================================================================
|
||
// Auto-pull hidden set — types the user has explicitly excluded from the
|
||
// REPOS{select: "all"} auto-pull. See AUTOPULL_HIDDEN_KEY in constants.js.
|
||
// =============================================================================
|
||
export function loadAutoPullHidden() {
|
||
try {
|
||
const raw = localStorage.getItem(AUTOPULL_HIDDEN_KEY);
|
||
if (!raw) return new Set();
|
||
const parsed = JSON.parse(raw);
|
||
return new Set(Array.isArray(parsed) ? parsed.filter(x => typeof x === "string") : []);
|
||
} catch (e) {
|
||
console.warn("[Koolook] failed to parse autoPullHidden; starting empty:", e);
|
||
return new Set();
|
||
}
|
||
}
|
||
|
||
function saveAutoPullHidden(set) {
|
||
try {
|
||
localStorage.setItem(AUTOPULL_HIDDEN_KEY, JSON.stringify([...set]));
|
||
return true;
|
||
} catch (e) {
|
||
console.warn("[Koolook] failed to save autoPullHidden:", e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export function hideAutoPullType(typeName) {
|
||
if (!typeName) return;
|
||
const set = loadAutoPullHidden();
|
||
if (set.has(typeName)) return;
|
||
set.add(typeName);
|
||
saveAutoPullHidden(set);
|
||
}
|
||
|
||
export function unhideAutoPullType(typeName) {
|
||
if (!typeName) return;
|
||
const set = loadAutoPullHidden();
|
||
if (!set.delete(typeName)) return;
|
||
saveAutoPullHidden(set);
|
||
}
|
||
|
||
// Bulk-replace the entire pick list. Used by the snapshot import flow —
|
||
// the caller has just deserialized a preset and wants picks to mirror it
|
||
// exactly. Filters non-strings defensively (snapshot files are user-
|
||
// editable and may have been hand-touched). Returns true on persist
|
||
// success, false on localStorage rejection (quota / private mode).
|
||
export function setAllPicks(picks) {
|
||
const cleaned = Array.isArray(picks)
|
||
? picks.filter(p => typeof p === "string")
|
||
: [];
|
||
return saveUserPicks(cleaned);
|
||
}
|
||
|
||
export function notifyPicksChanged() {
|
||
window.dispatchEvent(new CustomEvent(PICKS_CHANGED_EVENT));
|
||
}
|
||
|