diff --git a/web/gemini_prompt_ui.js b/web/gemini_prompt_ui.js
index 444dac0..29227ed 100644
--- a/web/gemini_prompt_ui.js
+++ b/web/gemini_prompt_ui.js
@@ -138,8 +138,16 @@ app.registerExtension({
const result = onExecuted?.apply(this, arguments);
if (this.status) {
+ // Remember the user's color only if no flash is already pending,
+ // otherwise we would capture our own temporary flash color
+ if (this.geminiFlashTimeout) {
+ clearTimeout(this.geminiFlashTimeout);
+ } else {
+ this.geminiPrevBgcolor = this.bgcolor;
+ }
+
// Check if there was an error in the output
- const outputs = message.output;
+ const outputs = message?.output;
if (outputs && outputs.prompt && outputs.prompt[0] && outputs.prompt[0].startsWith("Error:")) {
this.status.value = "Error - Check output";
this.bgcolor = "#552222";
@@ -148,17 +156,30 @@ app.registerExtension({
this.bgcolor = "#225522";
}
- // Reset color after delay
- setTimeout(() => {
- this.bgcolor = "";
+ // Restore the previous color after delay
+ this.geminiFlashTimeout = setTimeout(() => {
+ this.geminiFlashTimeout = null;
+ this.bgcolor = this.geminiPrevBgcolor;
+ this.geminiPrevBgcolor = undefined;
if (this.status) {
this.status.value = "Ready";
}
+ this.setDirtyCanvas?.(true, true);
}, 3000);
}
return result;
};
+
+ // Clear any pending color-restore timer when the node is removed
+ const onRemoved = nodeType.prototype.onRemoved;
+ nodeType.prototype.onRemoved = function() {
+ if (this.geminiFlashTimeout) {
+ clearTimeout(this.geminiFlashTimeout);
+ this.geminiFlashTimeout = null;
+ }
+ return onRemoved?.apply(this, arguments);
+ };
}
},
diff --git a/web/kiko_local_image_loader.js b/web/kiko_local_image_loader.js
index 1259e30..75e9349 100644
--- a/web/kiko_local_image_loader.js
+++ b/web/kiko_local_image_loader.js
@@ -1,6 +1,34 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
+// Escape a value for safe interpolation into HTML text or attribute contexts
+function escapeHtml(value) {
+ return String(value ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+// Build a readable error message from a non-OK fetch response
+async function describeHttpError(response) {
+ let detail = '';
+ try {
+ const body = await response.json();
+ detail = body?.error || body?.message || '';
+ } catch (e) {
+ // Non-JSON body; fall back to the status code
+ }
+ if (response.status === 403) {
+ return detail || 'Access denied: path is outside the allowed folders';
+ }
+ if (response.status === 400) {
+ return detail || 'Invalid path or request';
+ }
+ return detail || `Request failed (HTTP ${response.status})`;
+}
+
// Setup global lightbox for image preview
function setupGlobalLightbox() {
if (document.getElementById('kiko-image-lightbox')) return;
@@ -511,7 +539,22 @@ app.registerExtension({
const debouncedLayout = debounce(applyMasonryLayout, 20);
// Set up ResizeObserver for responsive layout
- new ResizeObserver(debouncedLayout).observe(cardHolder);
+ this.kikoResizeObserver = new ResizeObserver(debouncedLayout);
+ this.kikoResizeObserver.observe(cardHolder);
+
+ const originalOnRemoved = this.onRemoved;
+ this.onRemoved = function () {
+ if (this.kikoResizeObserver) {
+ this.kikoResizeObserver.disconnect();
+ this.kikoResizeObserver = null;
+ }
+ return originalOnRemoved?.apply(this, arguments);
+ };
+
+ const showStatus = (message) => {
+ statusMessage.style.display = 'block';
+ statusMessage.textContent = message;
+ };
// Autocomplete functionality
let autocompleteSelectedIndex = -1;
@@ -521,6 +564,10 @@ app.registerExtension({
async function fetchDirectorySuggestions(path) {
try {
const response = await api.fetchApi(`/kiko_local_image_loader/list_directories?path=${encodeURIComponent(path)}`);
+ if (!response.ok) {
+ // Disallowed or invalid paths simply yield no suggestions
+ return [];
+ }
const data = await response.json();
return data.directories || [];
} catch (error) {
@@ -545,11 +592,11 @@ app.registerExtension({
const parentPath = parts.length > 0 ? '/' + parts.join('/') : '';
return `
-
+
${smallFolderSVG}
- ${folderName}
- ${parentPath ? ` — ${parentPath}` : ''}
+ ${escapeHtml(folderName)}
+ ${parentPath ? ` — ${escapeHtml(parentPath)}` : ''}
`;
@@ -668,6 +715,9 @@ app.registerExtension({
async function loadSavedPaths() {
try {
const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
+ if (!response.ok) {
+ throw new Error(await describeHttpError(response));
+ }
const data = await response.json();
pathPresets.innerHTML = '
';
@@ -689,20 +739,27 @@ app.registerExtension({
try {
const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
+ if (!response.ok) {
+ throw new Error(await describeHttpError(response));
+ }
const data = await response.json();
const savedPaths = data.saved_paths || [];
if (!savedPaths.includes(currentDirectory)) {
savedPaths.push(currentDirectory);
- await api.fetchApi('/kiko_local_image_loader/save_paths', {
+ const saveResponse = await api.fetchApi('/kiko_local_image_loader/save_paths', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths: savedPaths })
});
+ if (!saveResponse.ok) {
+ throw new Error(await describeHttpError(saveResponse));
+ }
await loadSavedPaths();
}
} catch (error) {
console.error('Error saving path:', error);
+ showStatus(`Error saving path: ${error.message}`);
}
};
@@ -713,20 +770,27 @@ app.registerExtension({
try {
const response = await api.fetchApi('/kiko_local_image_loader/get_saved_paths');
+ if (!response.ok) {
+ throw new Error(await describeHttpError(response));
+ }
const data = await response.json();
const savedPaths = data.saved_paths || [];
const updatedPaths = savedPaths.filter(p => p !== selectedPath);
if (updatedPaths.length !== savedPaths.length) {
- await api.fetchApi('/kiko_local_image_loader/save_paths', {
+ const saveResponse = await api.fetchApi('/kiko_local_image_loader/save_paths', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths: updatedPaths })
});
+ if (!saveResponse.ok) {
+ throw new Error(await describeHttpError(saveResponse));
+ }
await loadSavedPaths();
}
} catch (error) {
console.error('Error deleting path:', error);
+ showStatus(`Error deleting path: ${error.message}`);
}
};
@@ -757,6 +821,10 @@ app.registerExtension({
try {
const response = await api.fetchApi(`/kiko_local_image_loader/images?${params}`);
+ if (!response.ok) {
+ statusMessage.textContent = await describeHttpError(response);
+ return;
+ }
const data = await response.json();
if (data.error) {
@@ -820,7 +888,7 @@ app.registerExtension({
`;
@@ -831,9 +899,9 @@ app.registerExtension({
const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`;
card.innerHTML = `
-
${item.name}
+
${escapeHtml(item.name)}
`;
// Trigger layout when image loads
@@ -856,10 +924,10 @@ app.registerExtension({
const thumbnailUrl = `/kiko_local_image_loader/thumbnail?filepath=${encodeURIComponent(item.path)}`;
card.innerHTML = `
-
${item.name}
+
${escapeHtml(item.name)}
`;
const img = card.querySelector('img');
@@ -882,7 +950,7 @@ app.registerExtension({
`;
@@ -924,7 +992,7 @@ app.registerExtension({
// Send to backend
try {
- await api.fetchApi('/kiko_local_image_loader/set_node_selection', {
+ const response = await api.fetchApi('/kiko_local_image_loader/set_node_selection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -933,11 +1001,16 @@ app.registerExtension({
type: type
})
});
+ if (!response.ok) {
+ showStatus(`Selection rejected: ${await describeHttpError(response)}`);
+ return;
+ }
// Trigger node update
node.setDirtyCanvas(true);
} catch (error) {
console.error('Error setting selection:', error);
+ showStatus('Error setting selection');
}
}
@@ -997,10 +1070,13 @@ app.registerExtension({
// Try to load last path
api.fetchApi('/kiko_local_image_loader/get_last_path').then(async response => {
+ if (!response.ok) return;
const data = await response.json();
if (data.last_path) {
loadImages(data.last_path);
}
+ }).catch(error => {
+ console.error('Error loading last path:', error);
});
return r;
diff --git a/web/kiko_save_image_ui.js b/web/kiko_save_image_ui.js
index 25d1f88..8bc45ac 100644
--- a/web/kiko_save_image_ui.js
+++ b/web/kiko_save_image_ui.js
@@ -377,6 +377,26 @@ function injectStyles() {
}
}
+// Escape a value for safe interpolation into HTML text or attribute contexts
+function escapeHtml(value) {
+ return String(value ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+// Build a ComfyUI /api/view URL with every parameter encoded
+function buildViewUrl(filename, subfolder, type) {
+ const params = new URLSearchParams({ filename: filename ?? '' });
+ if (subfolder && subfolder.trim()) {
+ params.set('subfolder', subfolder);
+ }
+ params.set('type', type || 'output');
+ return `${window.location.origin}/api/view?${params.toString()}`;
+}
+
// Format file size for display
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
@@ -388,8 +408,6 @@ function formatFileSize(bytes) {
// Open image in new tab
function openImageInTab(imagePath, subfolder = '', enablePopup = true) {
- console.log(`KikoSaveImage: Attempting to open image: ${imagePath}, subfolder: ${subfolder}`);
-
// Note: enablePopup parameter is kept for compatibility but not used
// since popup now controls viewer visibility, not individual image clicks
@@ -403,16 +421,12 @@ function openImageInTab(imagePath, subfolder = '', enablePopup = true) {
fullPath = `${basePath}/api/view?filename=${encodeURIComponent(imagePath)}&type=output`;
}
- console.log(`KikoSaveImage: Opening URL: ${fullPath}`);
-
// Extract clean filename for window name (remove timestamp and batch number)
const cleanName = imagePath.split('_').slice(0, -2).join('_') || 'KikoSaveImage';
// Try to open the image with a clean window name
const newWindow = window.open(fullPath, cleanName.replace(/[^a-zA-Z0-9]/g, '_'));
- if (newWindow) {
- console.log('KikoSaveImage: Successfully opened new window');
- } else {
+ if (!newWindow) {
console.error('KikoSaveImage: Failed to open new window - popup blocked?');
// Fallback: copy URL to clipboard and alert user
navigator.clipboard.writeText(fullPath).then(() => {
@@ -435,6 +449,10 @@ class KikoImageViewer extends HTMLElement {
this.dragOffset = { x: 0, y: 0 };
this.lastHeaderClick = 0;
+ // Bind document-level drag handlers once so they can be removed later
+ this.boundHandleDrag = this.handleDrag.bind(this);
+ this.boundEndDrag = this.endDrag.bind(this);
+
// Always start at default position
}
@@ -442,6 +460,16 @@ class KikoImageViewer extends HTMLElement {
return ['data'];
}
+ connectedCallback() {
+ document.addEventListener('mousemove', this.boundHandleDrag);
+ document.addEventListener('mouseup', this.boundEndDrag);
+ }
+
+ disconnectedCallback() {
+ document.removeEventListener('mousemove', this.boundHandleDrag);
+ document.removeEventListener('mouseup', this.boundEndDrag);
+ }
+
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data' && newValue) {
try {
@@ -455,8 +483,6 @@ class KikoImageViewer extends HTMLElement {
}
setImageData(data) {
- console.log('KikoImageViewer: setImageData called with:', data);
- console.log('KikoImageViewer: First image data:', data[0]);
this.imageData = data;
this.autoUnrollOnNewImages();
this.render();
@@ -513,9 +539,7 @@ class KikoImageViewer extends HTMLElement {
});
});
- // Global mouse events for dragging
- document.addEventListener('mousemove', this.handleDrag.bind(this));
- document.addEventListener('mouseup', this.endDrag.bind(this));
+ // Document-level drag listeners are managed in connectedCallback/disconnectedCallback
}
startDrag(e) {
@@ -697,12 +721,9 @@ class KikoImageViewer extends HTMLElement {
openAllImagesWithDelay() {
const selectedIndices = Array.from(this.selectedImages);
if (selectedIndices.length === 0) {
- console.log('KikoSaveImage: No images selected for opening');
return;
}
- console.log(`KikoSaveImage: Opening ${selectedIndices.length} images with delay`);
-
// Open images with 150ms delay between each to prevent popup blocking
selectedIndices.forEach((index, i) => {
setTimeout(() => {
@@ -716,12 +737,7 @@ class KikoImageViewer extends HTMLElement {
downloadImage(imageData) {
// Construct proper image URL handling subfolder
- let imageUrl;
- if (imageData.subfolder && imageData.subfolder.trim()) {
- imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(imageData.filename)}&subfolder=${encodeURIComponent(imageData.subfolder)}&type=${imageData.type}`;
- } else {
- imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(imageData.filename)}&type=${imageData.type}`;
- }
+ const imageUrl = buildViewUrl(imageData.filename, imageData.subfolder, imageData.type);
// Create temporary link and trigger download
const link = document.createElement('a');
@@ -748,7 +764,7 @@ class KikoImageViewer extends HTMLElement {
}
const header = this.imageData.length === 1
- ? `Saved Image (${this.imageData[0].format})`
+ ? `Saved Image (${escapeHtml(this.imageData[0].format)})`
: `Saved Images (${this.imageData.length} files)`;
const containerStyle = this.isMinimized ? 'style="display: none;"' : '';
@@ -791,15 +807,10 @@ class KikoImageViewer extends HTMLElement {
createImageItem(data, index) {
// Construct proper image URL handling subfolder
- let imageUrl;
- if (data.subfolder && data.subfolder.trim()) {
- // ComfyUI expects subfolder to be passed separately
- imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(data.filename)}&subfolder=${encodeURIComponent(data.subfolder)}&type=${data.type}`;
- } else {
- imageUrl = `${window.location.origin}/api/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}`;
- }
+ // ComfyUI expects subfolder to be passed separately
+ const imageUrl = buildViewUrl(data.filename, data.subfolder, data.type);
- const formatClass = `kiko-format-${data.format.toLowerCase()}`;
+ const formatClass = `kiko-format-${String(data.format ?? '').toLowerCase().replace(/[^a-z0-9_-]/g, '')}`;
// Format file size
const fileSize = data.file_size ? formatFileSize(data.file_size) : 'Unknown';
@@ -818,7 +829,7 @@ class KikoImageViewer extends HTMLElement {
return `
-

+
})
${this.imageData.length > 1 ? `
@@ -829,14 +840,14 @@ class KikoImageViewer extends HTMLElement {
-
- ${data.filename}
+
+ ${escapeHtml(data.filename)}
- ${data.format}
- ${data.dimensions || 'Unknown'} • ${fileSize}
- ${qualityInfo ? ` • ${qualityInfo}` : ''}
+ ${escapeHtml(data.format)}
+ ${escapeHtml(data.dimensions || 'Unknown')} • ${escapeHtml(fileSize)}
+ ${qualityInfo ? ` • ${escapeHtml(qualityInfo)}` : ''}
@@ -852,7 +863,6 @@ class KikoImageViewer extends HTMLElement {
item.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
- console.log(`KikoImageViewer: Clicking image ${index}: ${data.filename}`);
openImageInTab(data.filename, data.subfolder || '');
});
}
@@ -872,8 +882,7 @@ function createImagePreview(imageData) {
// Create image element
const img = document.createElement('img');
- const imagePath = `/api/view?filename=${imageData.filename}&type=${imageData.type}`;
- img.src = imagePath;
+ img.src = buildViewUrl(imageData.filename, imageData.subfolder, imageData.type);
img.alt = imageData.filename;
// Create info overlay
@@ -1012,17 +1021,13 @@ app.registerExtension({
// Replace the standard image viewer with our custom web component
const originalOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
- console.log('KikoSaveImage: onExecuted called with message:', message);
-
// Skip the original ComfyUI preview system
// Don't call originalOnExecuted to prevent default image display
// Use our custom web component instead
if (message && message.kiko_enhanced && message.kiko_enhanced.length > 0) {
- console.log('KikoSaveImage: Creating custom image viewer');
this.createCustomImageViewer(message.kiko_enhanced);
} else {
- console.log('KikoSaveImage: No enhanced data found, falling back to standard preview');
// Fall back to standard behavior if no enhanced data
if (originalOnExecuted) {
originalOnExecuted.apply(this, arguments);
@@ -1032,14 +1037,10 @@ app.registerExtension({
// Add method to create custom image viewer
nodeType.prototype.createCustomImageViewer = function(imageData) {
- console.log('KikoSaveImage: Creating custom viewer for', imageData.length, 'images');
-
// Check if popup is enabled for any image (use first image's popup setting)
const popupEnabled = imageData.length > 0 ? imageData[0].popup : true;
- console.log('KikoSaveImage: Popup enabled:', popupEnabled);
if (!popupEnabled) {
- console.log('KikoSaveImage: Popup disabled, not showing custom viewer');
return;
}
@@ -1049,7 +1050,6 @@ app.registerExtension({
// Update existing viewer with new data
existingViewer.setImageData(imageData);
existingViewer.show(); // Make sure it's visible
- console.log('KikoSaveImage: Updated existing viewer');
return;
}
@@ -1072,8 +1072,6 @@ app.registerExtension({
// Append to body (floating window) - always visible
document.body.appendChild(viewer);
-
- console.log('KikoSaveImage: Custom viewer created successfully');
};
// Add method to create toggle button
@@ -1137,50 +1135,16 @@ app.registerExtension({
nodeElement.appendChild(toggleButton);
this.kikoToggleButton = toggleButton;
-
- console.log('KikoSaveImage: Toggle button created');
};
// Legacy method kept for compatibility (not used with web component)
nodeType.prototype.enhanceImagePreviews = function(imageData) {
- console.log('KikoSaveImage: Legacy enhanceImagePreviews called (should use web component instead)');
+ // No-op: superseded by the kiko-image-viewer web component
};
}
}
});
-// Export helper functions to global scope for debugging
-window.kikoSaveImageTest = function() {
- console.log('KikoSaveImage: Testing click functionality...');
-
- // Find all images in the document
- const allImages = document.querySelectorAll('img');
- console.log(`Found ${allImages.length} images in document`);
-
- // Try to find images that look like our saved images
- allImages.forEach((img, index) => {
- console.log(`Image ${index}: src = ${img.src}`);
-
- // Add test click handler to all images
- img.style.border = '2px solid red';
- img.style.cursor = 'pointer';
- img.title = 'TEST: Click to open in new tab';
-
- // Remove old handlers and add new one
- const newImg = img.cloneNode(true);
- img.parentNode.replaceChild(newImg, img);
-
- newImg.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- console.log('TEST CLICK WORKED!', newImg.src);
- window.open(newImg.src, '_blank');
- }, true);
- });
-
- console.log('KikoSaveImage: Test setup complete. All images should now be clickable with red borders.');
-};
-
// 🎉 RESET VIEWER WINDOWS 🎉
window.kikoResetViewer = function() {
console.log('🎉 Resetting KikoSaveImage viewer windows...');
@@ -1219,4 +1183,3 @@ console.log('🎉 KikoSaveImage: Extension loaded!');
console.log('💡 Helper functions available:');
console.log(' 📞 kikoResetViewer() - Remove existing viewer windows');
console.log(' 🚀 kikoForceViewer() - Show demo viewer');
-console.log(' 🧪 kikoSaveImageTest() - Test image click functionality');
diff --git a/web/kiko_workflow_timer_ui.js b/web/kiko_workflow_timer_ui.js
index 8ae736f..cb049bd 100644
--- a/web/kiko_workflow_timer_ui.js
+++ b/web/kiko_workflow_timer_ui.js
@@ -448,8 +448,10 @@ const KikoWorkflowTimerExtension = {
KikoGlobalTimer.registerNode(this);
};
+ const origOnRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function() {
KikoGlobalTimer.unregisterNode(this);
+ return origOnRemoved?.apply(this, arguments);
};
const origOnSerialize = nodeType.prototype.onSerialize;
@@ -461,7 +463,7 @@ const KikoWorkflowTimerExtension = {
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
origOnConfigure?.apply(this, arguments);
- this.properties = info.properties || {};
+ this.properties = { ...(this.properties || {}), ...(info?.properties || {}) };
if (this.timerDisplay) {
this.timerDisplay.textContent = this.properties.elapsed_time_str || "00:00:000";
diff --git a/web/seed_history_ui.js b/web/seed_history_ui.js
index 2ebf6f8..e92cd79 100644
--- a/web/seed_history_ui.js
+++ b/web/seed_history_ui.js
@@ -371,14 +371,15 @@ app.registerExtension({
});
const timeAgo = this.formatTimeAgo(item.timestamp);
- entryDiv.innerHTML = `
-
- 🎲 ${item.seed}
-
-
- ⏰ ${timeAgo}
-
- `;
+ // Build with textContent: history comes from localStorage and is untrusted
+ const seedDiv = document.createElement("div");
+ seedDiv.style.cssText = "color: #fff; font-weight: bold; margin-bottom: 1px;";
+ seedDiv.textContent = `🎲 ${Number(item.seed)}`;
+ const timeDiv = document.createElement("div");
+ timeDiv.style.cssText = "color: #999; font-size: 8px;";
+ timeDiv.textContent = `⏰ ${timeAgo}`;
+ entryDiv.appendChild(seedDiv);
+ entryDiv.appendChild(timeDiv);
this.historyDisplay.appendChild(entryDiv);
});