Refactor. Improve light theme compatibility. Add offline 3D viewer & CSP support. Improve file deletion logic
This commit is contained in:
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user