diff --git a/README.md b/README.md
index 1b8344e..684575e 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,38 @@ The "Bridge" is a small extension for ComfyUI that adds a "Send to Downloader" b
2. Rename it to `comfyui-model-downloader`.
3. Restart ComfyUI.
+## βοΈ ComfyUI Process Control (Dynamic Discovery)
+
+The application can manage your ComfyUI server life cycle (Start/Stop/Restart) completely **autonomously**. It dynamically determines the correct paths and commands for **Windows, Linux, and macOS**.
+
+### How Discovery Works
+The app scans your system to find ComfyUI without manual input:
+1. **System Variables:** Evaluates `COMFYUI_PATH`, `COMFYUI_HOME`, or `PINOKIO_BIN`.
+2. **Context Recognition:** Detects if the downloader is running within or near a ComfyUI directory.
+3. **Environment Heuristics:** Automatically locates `.venv` or `python` environments and standard installation paths (like `%USERPROFILE%\ComfyUI` or Pinokio folders).
+4. **Automatic Port Sync:** Adjusts the launch command's port based on your configured Server URL.
+
+### Reference Examples (Auto-Detected)
+While usually not required, here is what the app typically discovers on different systems:
+
+#### πͺ Windows (Pinokio)
+* **Python:** `...\comfyuidata\.venv\Scripts\python.exe`
+* **Main:** `...\ComfyUI\resources\ComfyUI\main.py`
+* **Default Port:** `8000`
+
+#### π§ Linux (Manual Venv)
+* **Python:** `./venv/bin/python3`
+* **Main:** `./main.py`
+* **Default Port:** `8188`
+
+#### π macOS (Apple Silicon)
+* **Python:** `python3` (System)
+* **Main:** `/Users/name/ComfyUI/main.py`
+
+*Note: You can manually override these in **π Comfy Control -> Advanced Settings** if you have a non-standard setup.*
+
+---
+
## π Getting Started
Choose one of the following three ways to install and run the application:
diff --git a/__init__.py b/__init__.py
index 7725dc7..3fe4891 100644
--- a/__init__.py
+++ b/__init__.py
@@ -2,42 +2,48 @@
import server
import folder_paths
import os
+import asyncio
+import importlib
from aiohttp import web
-print("\033[95m[Model-Downloader] UI Extension Bridge active\033[0m")
+print("\033[95m[CMFD] UI Extension Bridge active\033[0m")
-USER_MODEL_PATH = r"C:\AI\comfyuidata\models"
-if os.path.exists(USER_MODEL_PATH):
- try:
- pass
- except: pass
-
-@server.PromptServer.instance.routes.post("/kippster/model-downloaded")
-async def model_downloaded(request):
- print("\033[92mπ₯ [Model-Downloader] PING ERHALTEN! Bereite UI-Refresh vor...\033[0m")
+@server.PromptServer.instance.routes.post("/cmfd/refresh-models")
+async def refresh_models(request):
+ data = await request.json() if request.has_body else {}
+ force_reload = data.get("force_reload", False)
+
+ print(f"\033[92mπ₯ [CMFD] Refresh signal received (Force Reload: {force_reload})\033[0m")
+
+ # Wait for OS
+ await asyncio.sleep(1)
try:
- # 1. Wir lΓΆschen nur die Caches, machen aber KEINEN eigenen Scan mehr!
- # Das verhindert, dass wir einen noch unfertigen Windows-Ordner scannen und den Fehler cachen.
+ # Invalidate
+ if hasattr(folder_paths, "invalidate_all_cached_folders"):
+ folder_paths.invalidate_all_cached_folders()
+
if hasattr(folder_paths, "filename_list_cache"):
folder_paths.filename_list_cache.clear()
- if hasattr(folder_paths, "cache_helper") and hasattr(folder_paths.cache_helper, "clear"):
- folder_paths.cache_helper.clear()
-
- if hasattr(folder_paths, "cache") and hasattr(folder_paths.cache, "clear"):
- folder_paths.cache.clear()
+ try:
+ importlib.reload(folder_paths)
+ except Exception:
+ pass
- # 2. Object Info zurΓΌcksetzen (zwingt ComfyUI beim nΓ€chsten UI-Klick zum Neuladen)
+ # Object Info Reset
server.PromptServer.instance.object_info = None
except Exception as e:
- print(f"[Model-Downloader] Fehler beim Cache-Reset: {e}")
+ print(f"[CMFD] Error: {e}")
- # 3. Signal an das Frontend senden
- server.PromptServer.instance.send_sync("kippster-refresh-ui", {"message": "ready"})
+ # One-way broadcast to frontend
+ server.PromptServer.instance.send_sync("cmfd-refresh-ui", {
+ "status": "ok",
+ "force_reload": force_reload
+ })
- return web.json_response({"status": "success"})
+ return web.json_response({"status": "ok"})
WEB_DIRECTORY = "web"
NODE_CLASS_MAPPINGS = {}
diff --git a/pom.xml b/pom.xml
index 521dada..88ddaac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,11 @@
flatlaf-extras
3.7.1
+
+ org.yaml
+ snakeyaml
+ 2.4
+
diff --git a/src/main/java/de/tki/comfymodels/Main.java b/src/main/java/de/tki/comfymodels/Main.java
index 74ad3e1..3f50306 100644
--- a/src/main/java/de/tki/comfymodels/Main.java
+++ b/src/main/java/de/tki/comfymodels/Main.java
@@ -11,6 +11,7 @@ import de.tki.comfymodels.service.impl.GeminiAIService;
import de.tki.comfymodels.service.impl.ModelListService;
import de.tki.comfymodels.service.impl.ModelHashRegistry;
import de.tki.comfymodels.ui.DotGridPanel;
+import de.tki.comfymodels.ui.ComfyWebPanel;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLightLaf;
import com.formdev.flatlaf.FlatLaf;
@@ -55,6 +56,9 @@ public class Main extends JFrame {
private final IModelValidator modelValidator;
private final de.tki.comfymodels.service.impl.RestBridgeService restBridge;
private final de.tki.comfymodels.service.impl.ArchiveService archiveService;
+ private final de.tki.comfymodels.service.IComfyLifecycleService lifecycleService;
+ private final de.tki.comfymodels.service.impl.ComfyDiagnosticService diagnosticService;
+ private de.tki.comfymodels.ui.ComfyWebPanel comfyWebPanel;
@Autowired
private ConfigService configService;
@@ -76,8 +80,10 @@ public class Main extends JFrame {
private JCheckBox backgroundCheck;
private JCheckBox shutdownCheck;
+ private JCheckBox restartCheck;
private JCheckBox darkCheck;
private JLabel activeAiModelLabel;
+ private JLabel lifecycleStatusLabel;
private JTextArea jsonInputArea;
private DefaultTableModel tableModel;
private JLabel statusLabel;
@@ -89,7 +95,9 @@ public class Main extends JFrame {
public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager,
IWorkflowService workflowService, IModelSearchService searchService,
IModelValidator modelValidator, de.tki.comfymodels.service.impl.RestBridgeService restBridge,
- de.tki.comfymodels.service.impl.ArchiveService archiveService) {
+ de.tki.comfymodels.service.impl.ArchiveService archiveService,
+ de.tki.comfymodels.service.IComfyLifecycleService lifecycleService,
+ de.tki.comfymodels.service.impl.ComfyDiagnosticService diagnosticService) {
this.analyzer = analyzer;
this.downloadManager = downloadManager;
this.workflowService = workflowService;
@@ -97,6 +105,8 @@ public class Main extends JFrame {
this.modelValidator = modelValidator;
this.restBridge = restBridge;
this.archiveService = archiveService;
+ this.lifecycleService = lifecycleService;
+ this.diagnosticService = diagnosticService;
}
public void launch(String[] args) {
@@ -148,6 +158,7 @@ public class Main extends JFrame {
if (configService.isBackgroundModeEnabled()) {
setVisible(false);
} else {
+ lifecycleService.stop();
downloadManager.stop();
System.exit(0);
}
@@ -282,10 +293,7 @@ public class Main extends JFrame {
MenuItem showItem = new MenuItem("Show UI");
showItem.addActionListener(e -> setVisible(true));
MenuItem exitItem = new MenuItem("Exit");
- exitItem.addActionListener(e -> {
- downloadManager.stop();
- System.exit(0);
- });
+ exitItem.addActionListener(e -> performAppExit());
popup.add(showItem);
popup.addSeparator();
@@ -350,6 +358,7 @@ public class Main extends JFrame {
try {
configService.unlock(pass);
+ configService.autoDiscoverPaths(); // Ensure paths are discovered after unlock
restBridge.setApiToken(configService.getApiToken());
syncBridgeFiles(); // NEW: Automatically sync code & token on every unlock
@@ -366,9 +375,110 @@ public class Main extends JFrame {
}
}
+ private void showLifecycleDialog() {
+ JDialog dialog = new JDialog(this, "ComfyUI Control", true);
+ dialog.setLayout(new BorderLayout());
+ dialog.setSize(880, 420);
+ dialog.setLocationRelativeTo(this);
+
+ JPanel content = new JPanel(new GridBagLayout());
+ content.setBorder(BorderFactory.createEmptyBorder(25, 25, 25, 25));
+ GridBagConstraints gbc = new GridBagConstraints();
+ gbc.fill = GridBagConstraints.HORIZONTAL;
+ gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0;
+
+ JLabel titleLabel = new JLabel("ComfyUI Server Status");
+ titleLabel.setFont(new Font("SansSerif", Font.BOLD, 18));
+ titleLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ content.add(titleLabel, gbc);
+
+ gbc.gridy++;
+ gbc.insets = new Insets(25, 0, 25, 0);
+ lifecycleStatusLabel = new JLabel(lifecycleService.getStatus());
+ lifecycleStatusLabel.setFont(new Font("SansSerif", Font.BOLD, 16));
+ lifecycleStatusLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ content.add(lifecycleStatusLabel, gbc);
+
+ gbc.gridy++;
+ gbc.insets = new Insets(10, 0, 20, 0);
+ JPanel controlButtons = new JPanel(new FlowLayout(FlowLayout.CENTER, 15, 10));
+ JButton startBtn = new JButton("βΆ Start");
+ startBtn.setPreferredSize(new Dimension(110, 40));
+ startBtn.addActionListener(e -> {
+ configService.autoDiscoverPaths();
+ startComfyAndReload();
+ });
+
+ JButton stopBtn = new JButton("βΉ Stop");
+ stopBtn.setPreferredSize(new Dimension(110, 40));
+ stopBtn.addActionListener(e -> lifecycleService.stop());
+
+ JButton restartBtn = new JButton("π Restart");
+ restartBtn.setPreferredSize(new Dimension(110, 40));
+ restartBtn.addActionListener(e -> {
+ configService.autoDiscoverPaths();
+ new Thread(() -> {
+ lifecycleService.stop();
+ try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
+ startComfyAndReload();
+ }).start();
+ });
+
+ JButton openBrowserBtn = new JButton("π Open Interface");
+ openBrowserBtn.setPreferredSize(new Dimension(150, 40));
+ openBrowserBtn.addActionListener(e -> {
+ try {
+ Desktop.getDesktop().browse(new java.net.URI(configService.getComfyUIUrl()));
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(dialog, "Could not open browser: " + ex.getMessage());
+ }
+ });
+
+ controlButtons.add(startBtn);
+ controlButtons.add(stopBtn);
+ controlButtons.add(restartBtn);
+ controlButtons.add(openBrowserBtn);
+ content.add(controlButtons, gbc);
+
+ // Timer to update status
+ Timer timer = new Timer(1000, e -> {
+ lifecycleStatusLabel.setText(lifecycleService.getStatus());
+ if (lifecycleService.isRunning()) {
+ lifecycleStatusLabel.setForeground(new Color(0, 150, 0));
+ startBtn.setEnabled(false);
+ stopBtn.setEnabled(true);
+ } else {
+ lifecycleStatusLabel.setForeground(Color.RED);
+ startBtn.setEnabled(true);
+ stopBtn.setEnabled(false);
+ }
+ });
+ timer.start();
+ dialog.addWindowListener(new java.awt.event.WindowAdapter() {
+ @Override public void windowClosing(java.awt.event.WindowEvent e) { timer.stop(); }
+ });
+
+ gbc.gridy++;
+ gbc.weighty = 1.0;
+ content.add(new JPanel(), gbc);
+
+ JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ bottom.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
+
+ JButton close = new JButton("Close");
+ close.addActionListener(e -> dialog.dispose());
+
+ bottom.add(close);
+
+ dialog.add(content, BorderLayout.CENTER);
+ dialog.add(bottom, BorderLayout.SOUTH);
+ dialog.setVisible(true);
+ }
+
private void loadSettingsIntoUI() {
if (backgroundCheck != null) backgroundCheck.setSelected(configService.isBackgroundModeEnabled());
if (shutdownCheck != null) shutdownCheck.setSelected(configService.isShutdownAfterDownloadEnabled());
+ if (restartCheck != null) restartCheck.setSelected(configService.isRestartAfterDownloadEnabled());
if (darkCheck != null) darkCheck.setSelected(configService.isDarkMode());
}
@@ -382,23 +492,42 @@ public class Main extends JFrame {
private void initUI() {
loadIcon();
setTitle("ComfyUIModel-Downloader");
- setSize(1450, 900);
+ setSize(1450, 950);
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
getRootPane().putClientProperty("flatlaf.useWindowDecorations", true);
- JPanel mainContainer = new DotGridPanel(new BorderLayout(10, 10));
- mainContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
- setContentPane(mainContainer);
+ // --- PREPARE TABBED INTERFACE ---
+ JTabbedPane mainTabs = new JTabbedPane();
+ mainTabs.setFont(new Font("SansSerif", Font.BOLD, 13));
+ comfyWebPanel = new de.tki.comfymodels.ui.ComfyWebPanel(configService.getComfyUIUrl());
- // --- NEW COMPACT TOOLBAR ---
+ // --- TAB 1: Model Manager (Original UI) ---
+ JPanel managerPanel = new JPanel(new BorderLayout(10, 10));
+ managerPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+ // --- TOOLBAR ---
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5));
toolbar.setBorder(BorderFactory.createEtchedBorder());
JButton settingsBtn = new JButton("β Settings...");
settingsBtn.addActionListener(e -> showSettingsMenu(settingsBtn));
+ JButton lifecycleBtn = new JButton("π Comfy Control...");
+ lifecycleBtn.addActionListener(e -> showLifecycleDialog());
+
+ JButton startServerBtn = new JButton("βΆ Start ComfyUI");
+ startServerBtn.setToolTipText("Starts the ComfyUI server in the background");
+ startServerBtn.addActionListener(e -> {
+ if (lifecycleService.isRunning()) {
+ JOptionPane.showMessageDialog(this, "ComfyUI is already running.", "Info", JOptionPane.INFORMATION_MESSAGE);
+ } else {
+ startComfyAndReload();
+ updateComfyTabVisibility(mainTabs);
+ }
+ });
+
JButton verifyBtn = new JButton("π Fast Verify");
verifyBtn.addActionListener(e -> verifyLocalModels(false));
@@ -419,10 +548,51 @@ public class Main extends JFrame {
JButton helpBtn = new JButton("βΉ Help");
helpBtn.addActionListener(e -> showHelpDialog());
+ JButton diagnosticBtn = new JButton("π©Ί Diagnostic");
+ diagnosticBtn.addActionListener(e -> {
+ if (modelsToDownload == null || modelsToDownload.isEmpty()) {
+ JOptionPane.showMessageDialog(this, "Load a workflow first to perform a diagnostic check.");
+ return;
+ }
+ List missing = diagnosticService.getMissingModels(modelsToDownload);
+ if (missing.isEmpty()) {
+ JOptionPane.showMessageDialog(this, "β
All workflow models are visible to ComfyUI!", "Diagnostic Passed", JOptionPane.INFORMATION_MESSAGE);
+ } else {
+ String list = String.join("\n- ", missing);
+
+ List