Refactor. Improve light theme compatibility. Add offline 3D viewer & CSP support. Improve file deletion logic

This commit is contained in:
m0rtus59
2026-09-10 21:57:14 +05:00
parent 3e7448dd80
commit 8343f216fc
29 changed files with 4086 additions and 3122 deletions
+152
View File
@@ -0,0 +1,152 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import { store } from "../core/store.js";
import { PromptStatus } from "../core/constants.js";
import { createPromptState, activatePrompt, updatePromptProgress } from "../core/promptState.js";
import { syncQueue, concludeRun, initSessionAndHistory, clearCancelledOrFailed } from "./queueService.js";
import { findImagesInOutputs, findTextsInOutputs } from "../utils/utils.js";
export function setupExecutionTracker(onTargetedProgressUpdate) {
const onStatus = () => syncQueue();
const onReconnected = async () => {
console.log("Comfy Sidebar: Server reconnected, syncing state and history");
await initSessionAndHistory();
await syncQueue();
};
const onExecutionStart = (e) => {
const autoClear = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Auto Clear Interrupted") ?? false;
if (autoClear) {
clearCancelledOrFailed();
}
const pid = String(e.detail.prompt_id);
store.setActivePromptId(pid);
const activeWorkspaceWorkflow = app.graph?.serialize ? app.graph.serialize() : null;
const existing = store.getPrompt(pid);
if (existing) {
activatePrompt(existing, { workflow: activeWorkspaceWorkflow });
store.updatePrompt(pid, existing);
} else {
const seq = store.nextSequence();
const newState = createPromptState(pid, {
status: PromptStatus.ACTIVE,
images: [],
progress: 0,
progressText: "Processing...",
timestamp: seq,
workflow: activeWorkspaceWorkflow,
startTime: Date.now()
});
store.setPrompt(pid, newState);
}
syncQueue();
};
const onProgress = (e) => {
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
if (pid && store.hasPrompt(pid)) {
const prompt = store.getPrompt(pid);
const percent = Math.round((e.detail.value / e.detail.max) * 100);
updatePromptProgress(prompt, percent);
store.updatePrompt(pid, prompt);
if (typeof onTargetedProgressUpdate === "function") {
onTargetedProgressUpdate(pid, percent, prompt.activeNodeName);
}
} else if (pid && !store.hasPrompt(pid)) {
syncQueue();
}
};
const onExecuting = (e) => {
const nodeId = e.detail;
const showWorkingNode = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Show Working Node Name") ?? true;
const activePid = store.ui.currentlyActivePromptId;
if (showWorkingNode && activePid && store.hasPrompt(activePid)) {
const prompt = store.getPrompt(activePid);
if (nodeId) {
const node = app.graph?.getNodeById ? app.graph.getNodeById(nodeId) : null;
prompt.activeNodeName = node ? (node.title || node.type) : `Node #${nodeId}`;
} else {
prompt.activeNodeName = "Finishing...";
}
store.updatePrompt(activePid, prompt);
if (typeof onTargetedProgressUpdate === "function") {
onTargetedProgressUpdate(activePid, prompt.progress, prompt.activeNodeName);
}
}
};
const onBPreview = (e) => {
const activeTasks = store.getAllPrompts().filter(t => t.status === PromptStatus.ACTIVE);
if (activeTasks.length > 0) {
const prompt = activeTasks[0];
const newBlobUrl = URL.createObjectURL(e.detail);
prompt._oldPreviewBlobUrl = prompt._previewBlobUrl;
prompt._previewBlobUrl = newBlobUrl;
prompt.images = [{ url: newBlobUrl }];
store.updatePrompt(prompt.pid, prompt);
}
};
const onExecuted = (e) => {
const pid = e.detail.prompt_id ? String(e.detail.prompt_id) : null;
if (pid && store.hasPrompt(pid)) {
const prompt = store.getPrompt(pid);
const nodeImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
if (nodeImgs.length > 0) {
prompt.images = nodeImgs;
}
const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
if (nodeTexts.length > 0) {
prompt.texts = nodeTexts;
}
if (!prompt.nodeOutputs) prompt.nodeOutputs = {};
prompt.nodeOutputs[e.detail.node] = e.detail.output;
store.updatePrompt(pid, prompt);
}
};
const onExecutionSuccess = (e) => concludeRun(e.detail.prompt_id, PromptStatus.COMPLETED);
const onExecutionError = (e) => concludeRun(e.detail.prompt_id, PromptStatus.ERROR);
const onExecutionInterrupted = () => {
store.getAllPrompts()
.filter(t => t.status === PromptStatus.ACTIVE)
.forEach(t => concludeRun(t.pid, PromptStatus.CANCELLED));
syncQueue();
};
api.addEventListener("status", onStatus);
api.addEventListener("reconnected", onReconnected);
api.addEventListener("execution_start", onExecutionStart);
api.addEventListener("progress", onProgress);
api.addEventListener("executing", onExecuting);
api.addEventListener("b_preview", onBPreview);
api.addEventListener("executed", onExecuted);
api.addEventListener("execution_success", onExecutionSuccess);
api.addEventListener("execution_error", onExecutionError);
api.addEventListener("execution_interrupted", onExecutionInterrupted);
return () => {
api.removeEventListener("status", onStatus);
api.removeEventListener("reconnected", onReconnected);
api.removeEventListener("execution_start", onExecutionStart);
api.removeEventListener("progress", onProgress);
api.removeEventListener("executing", onExecuting);
api.removeEventListener("b_preview", onBPreview);
api.removeEventListener("executed", onExecuted);
api.removeEventListener("execution_success", onExecutionSuccess);
api.removeEventListener("execution_error", onExecutionError);
api.removeEventListener("execution_interrupted", onExecutionInterrupted);
};
}
+75
View File
@@ -0,0 +1,75 @@
import { parseWorkflow } from "../utils/utils.js";
import { PromptStatus } from "../core/constants.js";
export function normalizeQueueItem(p, defaultIndex = 0) {
let pid = null;
let seq = defaultIndex;
let rawWorkflow = null;
let outputs = {};
if (Array.isArray(p)) {
seq = typeof p[0] === "number" ? p[0] : defaultIndex;
pid = p[1] ? String(p[1]) : null;
const extraData = p[3] || {};
rawWorkflow = extraData.extra_pnginfo?.workflow || extraData.workflow || null;
outputs = p[4] || {};
} else if (p && typeof p === "object") {
pid = (p.prompt_id || p.id || p.uuid) ? String(p.prompt_id || p.id || p.uuid) : null;
seq = typeof p.number === "number" ? p.number : (typeof p.prompt_number === "number" ? p.prompt_number : defaultIndex);
const extraData = p.extra_data || {};
rawWorkflow = extraData.extra_pnginfo?.workflow || extraData.workflow || null;
outputs = p.outputs || {};
}
return {
pid,
seq,
workflow: parseWorkflow(rawWorkflow),
outputs
};
}
export function normalizeHistoryItem(pidKey, rawData) {
const pid = String(pidKey);
let item = rawData;
if (Array.isArray(item)) {
item = {
prompt: item[0],
outputs: item[1] || {},
status: item[2] || {}
};
} else if (!item || typeof item !== "object") {
item = { outputs: {}, status: {} };
}
const extraData = item.extra_data || item.prompt?.[3] || {};
const rawWf = extraData.extra_pnginfo?.workflow || extraData.workflow || null;
const workflow = parseWorkflow(rawWf);
const outputs = item.outputs || {};
const queueNumber = item.prompt?.[0] ?? 0;
const serverStatus = item.status?.status_str || "completed";
const isInterrupted = serverStatus === "interrupted" ||
(item.status?.messages && JSON.stringify(item.status.messages).toLowerCase().includes("interrupted"));
let status = PromptStatus.COMPLETED;
if (serverStatus === "success" || serverStatus === "completed") {
status = PromptStatus.COMPLETED;
} else if (isInterrupted) {
status = PromptStatus.CANCELLED;
} else if (serverStatus === "error") {
status = PromptStatus.ERROR;
} else {
status = PromptStatus.CANCELLED;
}
return {
pid,
queueNumber,
workflow,
outputs,
status,
serverStatus
};
}
+306
View File
@@ -0,0 +1,306 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import { store } from "../core/store.js";
import { PromptStatus, StoreEvents } from "../core/constants.js";
import { createPromptState, activatePrompt, finalizePrompt } from "../core/promptState.js";
import { normalizeQueueItem, normalizeHistoryItem } from "./queueNormalizer.js";
import { findImagesInOutputs, findTextsInOutputs, getPrimaryOutputImages, getNodeTitle } from "../utils/utils.js";
function extractNodeTitles(outputs, workflow, promptGraph = null) {
const titles = {};
if (!outputs) return titles;
for (const nodeId in outputs) {
const title = getNodeTitle(nodeId, workflow, promptGraph);
if (title) {
titles[nodeId] = title;
}
}
return titles;
}
export async function syncQueue() {
try {
const q = await api.getQueue();
const runningList = q.Running || q.queue_running || [];
const pendingList = q.Pending || q.queue_pending || [];
const runningIds = new Set();
const pendingIds = new Set();
runningList.forEach((raw, idx) => {
const item = normalizeQueueItem(raw, idx);
if (!item.pid) return;
runningIds.add(item.pid);
store.setActivePromptId(item.pid);
const existing = store.getPrompt(item.pid);
const activeWorkflow = item.workflow || (app.graph?.serialize?.() || null);
const titles = extractNodeTitles(item.outputs, activeWorkflow);
if (!existing) {
const seq = store.nextSequence();
const images = getPrimaryOutputImages(item.outputs, activeWorkflow);
const texts = findTextsInOutputs(item.outputs, activeWorkflow);
const newState = createPromptState(item.pid, {
status: PromptStatus.ACTIVE,
images,
texts,
nodeOutputs: item.outputs,
nodeTitles: titles,
progress: 0,
progressText: "Processing...",
timestamp: seq,
workflow: activeWorkflow,
startTime: Date.now()
});
store.setPrompt(item.pid, newState);
} else {
if (existing.status !== PromptStatus.ACTIVE) {
activatePrompt(existing, { workflow: activeWorkflow, nodeTitles: titles });
store.updatePrompt(item.pid, existing);
} else {
if (!existing.workflow && activeWorkflow) existing.workflow = activeWorkflow;
if (Object.keys(titles).length > 0) Object.assign(existing.nodeTitles, titles);
store.updatePrompt(item.pid, existing);
}
}
});
const normalizedPending = pendingList
.map((raw, idx) => normalizeQueueItem(raw, idx))
.filter(item => Boolean(item.pid))
.sort((a, b) => a.seq - b.seq);
normalizedPending.forEach((item, index) => {
const number = index + 1;
pendingIds.add(item.pid);
const existing = store.getPrompt(item.pid);
if (!existing) {
const seq = store.nextSequence();
const newState = createPromptState(item.pid, {
status: PromptStatus.PENDING,
images: [],
progress: 0,
queueNumber: number,
progressText: `Pending... (#${number})`,
timestamp: seq,
workflow: app.graph?.serialize ? app.graph.serialize() : null
});
store.setPrompt(item.pid, newState);
} else if (existing.status === PromptStatus.PENDING) {
if (existing.queueNumber !== number) {
store.updatePrompt(item.pid, {
queueNumber: number,
progressText: `Pending... (#${number})`
});
}
}
});
for (const prompt of store.getAllPrompts()) {
if (prompt.status === PromptStatus.PENDING && !pendingIds.has(prompt.pid)) {
store.deletePrompt(prompt.pid);
} else if (prompt.status === PromptStatus.ACTIVE && !runningIds.has(prompt.pid)) {
concludeRun(prompt.pid, PromptStatus.CANCELLED);
}
}
store.emit(StoreEvents.QUEUE_SYNCED, {
pendingCount: pendingIds.size,
runningCount: runningIds.size
});
} catch (err) {
console.error("Comfy Sidebar: Failed to sync queue state", err);
}
}
export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED) {
const key = String(pid);
if (!key || !store.hasPrompt(key)) return;
if (store.ui.currentlyActivePromptId === key) {
store.setActivePromptId(null);
}
const state = store.getPrompt(key);
finalizePrompt(state, fallbackStatus);
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(`/history/${key}`);
const hItem = await res.json();
if (hItem && hItem[key]) {
const norm = normalizeHistoryItem(key, hItem[key]);
state.workflow = norm.workflow || state.workflow;
state.nodeOutputs = norm.outputs || state.nodeOutputs;
state.status = norm.status;
const promptGraph = hItem[key].prompt?.[2] || null;
const titles = extractNodeTitles(state.nodeOutputs, state.workflow, promptGraph);
Object.assign(state.nodeTitles, titles);
const primaryImgs = getPrimaryOutputImages(norm.outputs, state.workflow);
if (primaryImgs.length > 0) state.images = primaryImgs;
const fetchedTexts = findTextsInOutputs(norm.outputs, state.workflow);
if (fetchedTexts.length > 0) state.texts = fetchedTexts;
break;
}
} catch (err) {}
await new Promise(r => setTimeout(r, 200));
}
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
store.pruneHistory(maxItems);
store.updatePrompt(key, state);
syncQueue();
}
export async function initSessionAndHistory() {
try {
const historyData = await api.getHistory();
if (!historyData) return;
const rawEntries = [];
if (Array.isArray(historyData.History)) {
historyData.History.forEach(item => {
if (item && item.prompt_id) {
rawEntries.push({ pid: String(item.prompt_id), data: item });
}
});
} else if (typeof historyData === "object") {
Object.keys(historyData).forEach(pidKey => {
rawEntries.push({ pid: String(pidKey), data: historyData[pidKey] });
});
}
const normalized = rawEntries.map(e => ({
norm: normalizeHistoryItem(e.pid, e.data),
promptGraph: e.data.prompt?.[2] || null
}));
normalized.sort((a, b) => a.norm.queueNumber - b.norm.queueNumber);
normalized.forEach(({ norm: item, promptGraph }) => {
const images = getPrimaryOutputImages(item.outputs, item.workflow);
const texts = findTextsInOutputs(item.outputs, item.workflow);
const titles = extractNodeTitles(item.outputs, item.workflow, promptGraph);
if (images.length === 0 && texts.length === 0 && item.status === PromptStatus.COMPLETED) {
return;
}
const existing = store.getPrompt(item.pid);
if (existing) {
if (existing.status === PromptStatus.ACTIVE || existing.status === PromptStatus.PENDING) {
existing.status = item.status;
existing.images = images;
existing.texts = texts;
existing.nodeOutputs = item.outputs;
existing.workflow = item.workflow || existing.workflow;
Object.assign(existing.nodeTitles, titles);
existing.progressText = "";
existing.rendered = false;
store.updatePrompt(item.pid, existing);
} else {
if (!existing.workflow && item.workflow) existing.workflow = item.workflow;
if ((!existing.images || existing.images.length === 0) && images.length > 0) existing.images = images;
if (!existing.nodeOutputs || Object.keys(existing.nodeOutputs).length === 0) {
existing.nodeOutputs = item.outputs;
}
if (!existing.nodeTitles) existing.nodeTitles = {};
Object.assign(existing.nodeTitles, titles);
if ((!existing.texts || existing.texts.length === 0) && texts && texts.length > 0) existing.texts = texts;
store.updatePrompt(item.pid, existing);
}
return;
}
const seq = store.nextSequence();
const newState = createPromptState(item.pid, {
status: item.status,
images,
texts,
nodeOutputs: item.outputs,
nodeTitles: titles,
workflow: item.workflow,
progressText: "",
timestamp: seq,
rendered: true
});
store.setPrompt(item.pid, newState);
});
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
store.pruneHistory(maxItems);
await syncQueue();
} catch (err) {
console.error("Comfy Sidebar: Failed to initialize history from server API", err);
}
}
export async function cancelPendingTask(pid) {
try {
await api.fetchApi("/queue", { method: "POST", body: JSON.stringify({ delete: [pid] }) });
await syncQueue();
} catch (e) {
console.error("Comfy Sidebar: Failed to cancel pending task", e);
}
}
export async function cancelAllPending() {
try {
await api.fetchApi("/queue", { method: "POST", body: JSON.stringify({ clear: true }) });
await syncQueue();
} catch (e) {
console.error("Comfy Sidebar: Failed to cancel all pending tasks", e);
}
}
export async function interruptActive() {
try {
await api.interrupt();
await syncQueue();
} catch (e) {
console.error("Comfy Sidebar: Failed to interrupt active run", e);
}
}
export async function deleteHistoryItem(pid) {
try {
store.deletePrompt(pid);
await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ delete: [pid] }) });
await syncQueue();
} catch (e) {
console.error("Comfy Sidebar: Failed to delete history item", e);
}
}
export async function clearAllHistory() {
try {
store.clearFinished();
await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ clear: true }) });
await syncQueue();
} catch (e) {
console.error("Comfy Sidebar: Failed to clear all history", e);
}
}
export async function clearCancelledOrFailed() {
try {
const toDelete = store.clearCancelledOrFailed();
if (toDelete.length > 0) {
await api.fetchApi("/history", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ delete: toDelete })
});
}
await syncQueue();
} catch (e) {
console.error("Comfy Sidebar: Failed to clear cancelled or failed tasks", e);
}
}