This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 47d0de5f0d03 camel-tui: Add run options form, properties editor, and 
F2 action improvements (#23444)
47d0de5f0d03 is described below

commit 47d0de5f0d0364195d5cfb4ca4e0c47f8cb4cf09
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri May 22 07:00:22 2026 +0200

    camel-tui: Add run options form, properties editor, and F2 action 
improvements (#23444)
    
    * camel-tui: Add Doctor dialog to F2 actions menu
    
    Runs 7 environment checks (Java, Camel, JBang, Maven, Container, Ports,
    Disk Space) and displays results in a centered popup with emoji status
    indicators.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * camel-tui: Add emoji icons to F2 actions menu items
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * camel-tui: Add Stop All to F2 actions menu
    
    When only integrations or only infra services are running, stops them
    immediately. When both are running, shows a checkbox dialog to select
    which groups to stop.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * camel-tui: Add caption overlay with typewriter effect
    
    Adds a caption feature for recordings and education that displays
    text with a typewriter animation, holds, then fades out.
    Triggered via Ctrl+T or F2 menu. Supports multi-line via \n.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * camel-tui: Add classpath viewer to F2 actions menu
    
    Adds a "Show Classpath" action that fetches the JARs from a running
    integration via the jvm dev console and displays them in a scrollable
    list with parsed Maven GAV coordinates. Camel JARs are highlighted.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * camel-tui: Add reusable fuzzy filter with inline search
    
    Adds FuzzyFilter utility for fuzzy matching with character-level
    highlighting. Integrates into the classpath viewer so typing
    immediately filters the list with matched characters highlighted.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * camel-tui: Replace name input with expanded run options form
    
    The Run dialog now provides Name, Port, Max seconds text fields
    and Dev mode, Observe, Backlog trace checkboxes instead of just
    a name input. Tab navigates between fields, Space toggles checkboxes.
    
    Co-Authored-By: Claude <[email protected]>
    
    * camel-jbang: Add application.properties to bundled examples
    
    Use property placeholders in timer-log, cron-log, rest-api, and
    routes examples so their values are configurable via properties.
    
    Co-Authored-By: Claude <[email protected]>
    
    * camel-tui: Add properties editor page to run options form
    
    The Run dialog now has a second page for editing application.properties
    from the selected example. Properties are loaded from bundled resources
    or downloaded from GitHub for online examples. Modified values are
    passed as --prop=key=value overrides when launching.
    
    Co-Authored-By: Claude <[email protected]>
    
    * camel-tui: Add custom property support with + key in run form
    
    Press + on the properties page to add a new key:value property.
    Tab switches between key and value fields. Backspace on an empty
    key removes the entry. Custom properties are passed as --prop=key=value.
    
    Co-Authored-By: Claude <[email protected]>
    
    * camel-tui: Add cycling duration mode in run options form
    
    The Max field now cycles between Max seconds, Max messages, and
    Max idle secs with Space. The corresponding --max-seconds,
    --max-messages, or --max-idle-seconds flag is passed when launching.
    
    Co-Authored-By: Claude <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../camel/cli/connector/LocalCliConnector.java     |  14 +
 .../examples/camel-jbang-example-catalog.json      |   4 +
 .../examples/cron-log/application.properties       |  20 +
 .../examples/cron-log/cron-log.camel.yaml          |   4 +-
 .../examples/rest-api/application.properties       |  20 +
 .../examples/rest-api/rest-api.camel.yaml          |   2 +-
 .../examples/routes/application.properties         |  18 +
 .../src/main/resources/examples/routes/beans.yaml  |   2 +-
 .../examples/timer-log/application.properties      |  20 +
 .../examples/timer-log/timer-log.camel.yaml        |   4 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 216 ++++---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  24 +
 .../jbang/core/commands/tui/CaptionOverlay.java    | 217 ++++++++
 .../jbang/core/commands/tui/ClasspathPopup.java    | 304 ++++++++++
 .../dsl/jbang/core/commands/tui/DoctorPopup.java   | 283 ++++++++++
 .../dsl/jbang/core/commands/tui/FuzzyFilter.java   | 125 +++++
 .../jbang/core/commands/tui/RunOptionsForm.java    | 618 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/StopAllPopup.java  | 228 ++++++++
 18 files changed, 2041 insertions(+), 82 deletions(-)

diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 7eb379358811..3f5f0c10b711 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -302,6 +302,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionDebugTask(root);
             } else if ("reset-stats".equals(action)) {
                 doActionResetStatsTask();
+            } else if ("jvm".equals(action)) {
+                doActionJvmTask();
             } else if ("thread-dump".equals(action)) {
                 doActionThreadDumpTask();
             } else if ("top-processors".equals(action)) {
@@ -757,6 +759,18 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionJvmTask() throws IOException {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("jvm");
+        if (dc != null) {
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionThreadDumpTask() throws IOException {
         DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("thread");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
index a9b467615531..4e42f22e97d8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
@@ -96,6 +96,7 @@
         "hasCitrusTests": false,
         "files": [
             "README.md",
+            "application.properties",
             "cron-log.camel.yaml"
         ]
     },
@@ -325,6 +326,7 @@
         "hasCitrusTests": false,
         "files": [
             "README.md",
+            "application.properties",
             "rest-api.camel.yaml"
         ]
     },
@@ -344,6 +346,7 @@
         "files": [
             "Greeter.java",
             "README.md",
+            "application.properties",
             "beans.yaml",
             "routes.camel.yaml"
         ]
@@ -431,6 +434,7 @@
         "hasCitrusTests": false,
         "files": [
             "README.md",
+            "application.properties",
             "timer-log.camel.yaml"
         ]
     },
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/application.properties
new file mode 100644
index 000000000000..a60bfaac62a6
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/application.properties
@@ -0,0 +1,20 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+##      http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+# Timer period in milliseconds
+cron.period=5000
+# Log message prefix
+cron.message=Scheduled task running at
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/cron-log.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/cron-log.camel.yaml
index fd729c9f5ea0..2ccc3b93c7c9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/cron-log.camel.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/cron-log.camel.yaml
@@ -20,8 +20,8 @@
     from:
       uri: timer:cron
       parameters:
-        period: "5000"
+        period: "{{cron.period}}"
       steps:
         - setBody:
-            simple: "Scheduled task running at ${date:now:HH:mm:ss}"
+            simple: "{{cron.message}} ${date:now:HH:mm:ss}"
         - log: "${body}"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/application.properties
new file mode 100644
index 000000000000..7f4c4e3284c3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/application.properties
@@ -0,0 +1,20 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+##      http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+# HTTP server port
+camel.server.port=8080
+# Greeting message for the hello endpoint
+greeting.message=Hello from Camel REST API!
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml
index dda5c6cba0fc..8bf9e16560e0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml
@@ -28,7 +28,7 @@
       uri: direct:hello
       steps:
         - setBody:
-            constant: "Hello from Camel REST API!"
+            constant: "{{greeting.message}}"
 - route:
     id: hello-name
     from:
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/application.properties
new file mode 100644
index 000000000000..f562742fc8e5
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/application.properties
@@ -0,0 +1,18 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+##      http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+# Greeting message used by the Greeter bean
+greeter.message=Hello!
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/beans.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/beans.yaml
index 70bc6a6d5d35..6b71f2f79d5d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/beans.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/beans.yaml
@@ -19,4 +19,4 @@
   - name: "greeter"
     type: "camel.example.Greeter"
     properties:
-      message: 'Hello!'
+      message: '{{greeter.message}}'
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/application.properties
new file mode 100644
index 000000000000..8f6509eb6b08
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/application.properties
@@ -0,0 +1,20 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+##      http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+# Timer period in milliseconds
+timer.period=1000
+# Greeting message to log
+greeting.message=Hello Camel!
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml
index 48124209c60d..6a40ad378d2e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml
@@ -20,8 +20,8 @@
     from:
       uri: timer:tick
       parameters:
-        period: "1000"
+        period: "{{timer.period}}"
       steps:
         - setBody:
-            simple: "Hello Camel! (message 
#${exchangeProperty.CamelTimerCounter})"
+            simple: "{{greeting.message}}"
         - log: "${body}"
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index bb12dfcec2b6..2db68d8234be 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -40,13 +40,10 @@ import dev.tamboui.widgets.Clear;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Title;
-import dev.tamboui.widgets.input.TextInput;
-import dev.tamboui.widgets.input.TextInputState;
 import dev.tamboui.widgets.list.ListItem;
 import dev.tamboui.widgets.list.ListState;
 import dev.tamboui.widgets.list.ListWidget;
 import dev.tamboui.widgets.list.ScrollMode;
-import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
 import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
@@ -59,9 +56,13 @@ class ActionsPopup {
 
     private static final int ACTION_RUN_EXAMPLE = 0;
     private static final int ACTION_SHOW_DOCS = 1;
-    private static final int ACTION_SCREENSHOT = 2;
-    private static final int ACTION_SHOW_KEYSTROKES = 3;
-    private static final int ACTION_COUNT = 4;
+    private static final int ACTION_CAPTION = 2;
+    private static final int ACTION_SCREENSHOT = 3;
+    private static final int ACTION_SHOW_KEYSTROKES = 4;
+    private static final int ACTION_DOCTOR = 5;
+    private static final int ACTION_CLASSPATH = 6;
+    private static final int ACTION_STOP_ALL = 7;
+    private static final int ACTION_COUNT = 8;
 
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
@@ -77,8 +78,7 @@ class ActionsPopup {
     private final ListState exampleBrowserState = new ListState();
     private List<JsonObject> exampleCatalog;
 
-    private boolean showNameInput;
-    private TextInputState nameInputState;
+    private final RunOptionsForm runOptionsForm = new RunOptionsForm();
     private JsonObject selectedExample;
 
     private boolean showDocPicker;
@@ -90,18 +90,26 @@ class ActionsPopup {
     private String docTitle;
     private int docScroll;
 
+    private final DoctorPopup doctorPopup = new DoctorPopup();
+    private final ClasspathPopup classpathPopup = new ClasspathPopup();
+    private final StopAllPopup stopAllPopup;
+    private final CaptionOverlay captionOverlay;
+
     private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
     private String launchNotification;
     private boolean launchNotificationError;
     private long launchNotificationExpiry;
 
     ActionsPopup(Supplier<Set<String>> runningNames, 
Supplier<List<IntegrationInfo>> integrations,
+                 Supplier<List<InfraInfo>> infraServices, CaptionOverlay 
captionOverlay,
                  Runnable screenshotAction, Runnable toggleKeystrokes, 
Supplier<Boolean> keystrokesEnabled) {
         this.runningNames = runningNames;
         this.integrations = integrations;
+        this.captionOverlay = captionOverlay;
         this.screenshotAction = screenshotAction;
         this.toggleKeystrokes = toggleKeystrokes;
         this.keystrokesEnabled = keystrokesEnabled;
+        this.stopAllPopup = new StopAllPopup(integrations, infraServices);
     }
 
     void setContext(MonitorContext ctx) {
@@ -109,7 +117,9 @@ class ActionsPopup {
     }
 
     boolean isVisible() {
-        return showActionsMenu || showExampleBrowser || showNameInput || 
showDocPicker || showDocViewer;
+        return showActionsMenu || showExampleBrowser || 
runOptionsForm.isVisible() || showDocPicker || showDocViewer
+                || doctorPopup.isVisible() || classpathPopup.isVisible()
+                || stopAllPopup.isVisible() || captionOverlay.isInputVisible();
     }
 
     void open() {
@@ -120,9 +130,13 @@ class ActionsPopup {
     void close() {
         showActionsMenu = false;
         showExampleBrowser = false;
-        showNameInput = false;
+        runOptionsForm.close();
         showDocPicker = false;
         showDocViewer = false;
+        doctorPopup.close();
+        classpathPopup.close();
+        stopAllPopup.close();
+        captionOverlay.close();
     }
 
     String notification() {
@@ -165,26 +179,14 @@ class ActionsPopup {
             }
             return true;
         }
-        if (showNameInput) {
+        if (runOptionsForm.isVisible()) {
             if (ke.isCancel()) {
-                showNameInput = false;
+                runOptionsForm.close();
                 showExampleBrowser = true;
             } else if (ke.isConfirm()) {
                 launchWithName();
-            } else if (ke.isDeleteBackward()) {
-                nameInputState.deleteBackward();
-            } else if (ke.isDeleteForward()) {
-                nameInputState.deleteForward();
-            } else if (ke.isLeft()) {
-                nameInputState.moveCursorLeft();
-            } else if (ke.isRight()) {
-                nameInputState.moveCursorRight();
-            } else if (ke.isHome()) {
-                nameInputState.moveCursorToStart();
-            } else if (ke.isEnd()) {
-                nameInputState.moveCursorToEnd();
-            } else if (ke.code() == KeyCode.CHAR) {
-                nameInputState.insert(ke.character());
+            } else {
+                runOptionsForm.handleKeyEvent(ke);
             }
             return true;
         }
@@ -209,6 +211,19 @@ class ActionsPopup {
             }
             return true;
         }
+        if (captionOverlay.isInputVisible()) {
+            return captionOverlay.handleKeyEvent(ke);
+        }
+        if (classpathPopup.handleKeyEvent(ke)) {
+            return true;
+        }
+        if (stopAllPopup.handleKeyEvent(ke)) {
+            checkStopAllNotification();
+            return true;
+        }
+        if (doctorPopup.handleKeyEvent(ke)) {
+            return true;
+        }
         if (showActionsMenu) {
             if (ke.isCancel()) {
                 showActionsMenu = false;
@@ -229,6 +244,19 @@ class ActionsPopup {
                     } else if (sel == ACTION_SHOW_KEYSTROKES) {
                         showActionsMenu = false;
                         toggleKeystrokes.run();
+                    } else if (sel == ACTION_DOCTOR) {
+                        showActionsMenu = false;
+                        doctorPopup.open();
+                    } else if (sel == ACTION_CLASSPATH) {
+                        showActionsMenu = false;
+                        openClasspath();
+                    } else if (sel == ACTION_STOP_ALL) {
+                        showActionsMenu = false;
+                        stopAllPopup.open();
+                        checkStopAllNotification();
+                    } else if (sel == ACTION_CAPTION) {
+                        showActionsMenu = false;
+                        captionOverlay.openInput();
                     }
                 }
             }
@@ -244,8 +272,8 @@ class ActionsPopup {
         if (showExampleBrowser) {
             renderExampleBrowser(frame, area);
         }
-        if (showNameInput) {
-            renderNameInput(frame, area);
+        if (runOptionsForm.isVisible()) {
+            runOptionsForm.render(frame, area);
         }
         if (showDocPicker) {
             renderDocPicker(frame, area);
@@ -253,9 +281,37 @@ class ActionsPopup {
         if (showDocViewer) {
             renderDocViewer(frame, area);
         }
+        if (doctorPopup.isVisible()) {
+            doctorPopup.render(frame, area);
+        }
+        if (stopAllPopup.isVisible()) {
+            stopAllPopup.render(frame, area);
+        }
+        if (classpathPopup.isVisible()) {
+            classpathPopup.render(frame, area);
+        }
+        if (captionOverlay.isInputVisible()) {
+            captionOverlay.render(frame, area);
+        }
     }
 
     void renderFooter(List<Span> spans) {
+        if (captionOverlay.isInputVisible()) {
+            captionOverlay.renderFooter(spans);
+            return;
+        }
+        if (classpathPopup.isVisible()) {
+            classpathPopup.renderFooter(spans);
+            return;
+        }
+        if (stopAllPopup.isVisible()) {
+            stopAllPopup.renderFooter(spans);
+            return;
+        }
+        if (doctorPopup.isVisible()) {
+            doctorPopup.renderFooter(spans);
+            return;
+        }
         if (showDocViewer) {
             hint(spans, "↑↓", "scroll");
             hintLast(spans, "Esc", "back");
@@ -267,9 +323,8 @@ class ActionsPopup {
             hintLast(spans, "Esc", "back");
             return;
         }
-        if (showNameInput) {
-            hint(spans, "Enter", "launch");
-            hintLast(spans, "Esc", "back");
+        if (runOptionsForm.isVisible()) {
+            runOptionsForm.renderFooter(spans);
             return;
         }
         if (showExampleBrowser) {
@@ -297,21 +352,29 @@ class ActionsPopup {
     // ---- Rendering ----
 
     private void renderActionsMenu(Frame frame, Rect area) {
-        int popupW = 32;
+        int popupW = 34;
         int popupH = 2 + ACTION_COUNT;
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
         int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
 
         frame.renderWidget(Clear.INSTANCE, popup);
+        // extra space after ⌨️ because it renders narrower than other emoji
         String keystrokeLabel = keystrokesEnabled.get()
-                ? "  Hide Keystrokes"
-                : "  Show Keystrokes";
+                ? "  ⌨️  Hide Keystrokes"
+                : "  ⌨️  Show Keystrokes";
+        String stopLabel = stopAllPopup.hasBothGroups()
+                ? "  🛑 Stop All..."
+                : "  🛑 Stop All";
         ListWidget list = ListWidget.builder()
-                .items(ListItem.from("  Run an example..."),
-                        ListItem.from("  Show Documentation"),
-                        ListItem.from("  Take Screenshot"),
-                        ListItem.from(keystrokeLabel))
+                .items(ListItem.from("  🐪 Run an example..."),
+                        ListItem.from("  📖 Show Documentation"),
+                        ListItem.from("  💬 Caption... (Ctrl+T)"),
+                        ListItem.from("  📸 Take Screenshot"),
+                        ListItem.from(keystrokeLabel),
+                        ListItem.from("  🩺 Run Doctor"),
+                        ListItem.from("  📦 Show Classpath"),
+                        ListItem.from(stopLabel))
                 .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
                 .highlightSymbol("")
                 .scrollMode(ScrollMode.NONE)
@@ -397,35 +460,6 @@ class ActionsPopup {
         return items;
     }
 
-    private void renderNameInput(Frame frame, Rect area) {
-        int popupW = Math.min(50, area.width() - 4);
-        int popupH = 4;
-        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
-        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
-
-        frame.renderWidget(Clear.INSTANCE, popup);
-
-        Block block = Block.builder()
-                .borderType(BorderType.ROUNDED)
-                .title(" Name ")
-                .titleBottom(Title.from(Line.from(
-                        Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" launch │"),
-                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
-                .build();
-        frame.renderWidget(block, popup);
-
-        Rect inner = new Rect(popup.left() + 2, popup.top() + 1, popup.width() 
- 4, 1);
-        frame.renderWidget(Paragraph.from(Line.from(
-                Span.styled("Name for the integration:", Style.EMPTY.dim()))), 
inner);
-
-        Rect inputArea = new Rect(popup.left() + 2, popup.top() + 2, 
popup.width() - 4, 1);
-        TextInput textInput = TextInput.builder()
-                .cursorStyle(Style.EMPTY.reversed())
-                .build();
-        frame.renderStatefulWidget(textInput, inputArea, nameInputState);
-    }
-
     // ---- Doc Viewer & Picker ----
 
     private void renderDocViewer(Frame frame, Rect area) {
@@ -583,6 +617,36 @@ class ActionsPopup {
         launchNotificationExpiry = System.currentTimeMillis() + 10000;
     }
 
+    private void checkStopAllNotification() {
+        String msg = stopAllPopup.consumeNotification();
+        if (msg != null) {
+            setNotification(msg, false);
+        }
+    }
+
+    private void openClasspath() {
+        if (ctx == null) {
+            return;
+        }
+        String pid = ctx.selectedPid;
+        if (pid == null) {
+            List<IntegrationInfo> ints = integrations.get();
+            List<IntegrationInfo> alive = ints.stream().filter(i -> 
!i.vanishing && i.pid != null).toList();
+            if (alive.size() == 1) {
+                pid = alive.get(0).pid;
+            }
+        }
+        if (pid == null) {
+            setNotification("Select an integration first", true);
+            return;
+        }
+        classpathPopup.open(ctx, pid, ctx.selectedName());
+        String err = classpathPopup.consumeError();
+        if (err != null) {
+            setNotification(err, true);
+        }
+    }
+
     // ---- Name Input ----
 
     private void openNameInput() {
@@ -598,8 +662,7 @@ class ActionsPopup {
         String baseName = example.getStringOrDefault("name", "");
         String autoName = generateUniqueName(baseName);
         showExampleBrowser = false;
-        showNameInput = true;
-        nameInputState = new TextInputState(autoName);
+        runOptionsForm.open(autoName, baseName, 
ExampleHelper.isBundled(example));
     }
 
     private String generateUniqueName(String baseName) {
@@ -616,26 +679,27 @@ class ActionsPopup {
     }
 
     private void launchWithName() {
-        if (selectedExample == null || nameInputState == null) {
+        if (selectedExample == null) {
             return;
         }
-        String customName = nameInputState.text().trim();
         String exampleName = selectedExample.getStringOrDefault("name", "");
-        showNameInput = false;
+        String displayName = runOptionsForm.name();
+        if (displayName.isEmpty()) {
+            displayName = exampleName;
+        }
+        List<String> extraArgs = runOptionsForm.buildArgs();
+        runOptionsForm.close();
         try {
             List<String> cmd = new 
ArrayList<>(LauncherHelper.getCamelCommand());
             cmd.add("run");
             cmd.add("--example=" + exampleName);
-            if (!customName.isEmpty() && !customName.equals(exampleName)) {
-                cmd.add("--name=" + customName);
-            }
+            cmd.addAll(extraArgs);
             Path outputFile = Files.createTempFile("camel-example-", ".log");
             outputFile.toFile().deleteOnExit();
             ProcessBuilder pb = new ProcessBuilder(cmd);
             pb.redirectErrorStream(true);
             pb.redirectOutput(outputFile.toFile());
             Process process = pb.start();
-            String displayName = customName.isEmpty() ? exampleName : 
customName;
             pendingLaunches.add(new PendingLaunch(displayName, process, 
outputFile, System.currentTimeMillis()));
             launchNotification = "Starting: " + displayName;
             launchNotificationError = false;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index f34daf997371..72a1265c6d39 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -202,6 +202,7 @@ public class CamelMonitor extends CamelCommand {
     private volatile boolean pendingScreenshot;
     private boolean recording;
     private final List<KeyRecord> recentKeys = new ArrayList<>();
+    private final CaptionOverlay captionOverlay = new CaptionOverlay();
 
     private final ActionsPopup actionsPopup = new ActionsPopup(
             () -> data.get().stream()
@@ -211,6 +212,10 @@ public class CamelMonitor extends CamelCommand {
             () -> data.get().stream()
                     .filter(i -> !i.vanishing)
                     .collect(Collectors.toList()),
+            () -> infraData.get().stream()
+                    .filter(i -> !i.vanishing)
+                    .collect(Collectors.toList()),
+            captionOverlay,
             () -> pendingScreenshot = true,
             () -> recording = !recording,
             () -> recording);
@@ -301,6 +306,14 @@ public class CamelMonitor extends CamelCommand {
                     recentKeys.add(new KeyRecord(label, 
System.currentTimeMillis()));
                 }
             }
+            if (captionOverlay.isCaptionVisible()) {
+                captionOverlay.handleKeyEvent(ke);
+                return true;
+            }
+            if (ke.hasCtrl() && ke.isChar('t')) {
+                captionOverlay.openInput();
+                return true;
+            }
             if (actionsPopup.isVisible()) {
                 return actionsPopup.handleKeyEvent(ke);
             }
@@ -535,6 +548,7 @@ public class CamelMonitor extends CamelCommand {
         if (event instanceof TickEvent) {
             long now = System.currentTimeMillis();
             actionsPopup.tick(now);
+            captionOverlay.tick(now);
             if (recording && !recentKeys.isEmpty()) {
                 long cutoff = now - 2000;
                 recentKeys.removeIf(k -> k.timestamp() < cutoff);
@@ -744,6 +758,9 @@ public class CamelMonitor extends CamelCommand {
             renderKillConfirm(frame, mainChunks.get(4));
         }
         actionsPopup.render(frame, mainChunks.get(4));
+        if (captionOverlay.isCaptionVisible()) {
+            captionOverlay.render(frame, mainChunks.get(4));
+        }
         renderFooter(frame, mainChunks.get(5));
 
         lastBuffer = frame.buffer();
@@ -1567,6 +1584,13 @@ public class CamelMonitor extends CamelCommand {
         screenshotMessage = null;
 
         List<Span> spans = new ArrayList<>();
+
+        if (captionOverlay.isCaptionVisible()) {
+            captionOverlay.renderFooter(spans);
+            frame.renderWidget(Paragraph.from(Line.from(spans)), area);
+            return;
+        }
+
         MonitorTab tab = activeTab();
 
         if (tab != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
new file mode 100644
index 000000000000..6574e4eb640d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class CaptionOverlay {
+
+    private static final long CHAR_DELAY_MS = 50;
+    private static final long HOLD_DURATION_MS = 3000;
+    private static final long FADE_DURATION_MS = 1000;
+
+    private boolean showInput;
+    private TextInputState inputState;
+
+    private String captionText;
+    private long captionStartTime;
+    private long captionFullyTypedTime;
+
+    boolean isInputVisible() {
+        return showInput;
+    }
+
+    boolean isCaptionVisible() {
+        return captionText != null;
+    }
+
+    boolean isVisible() {
+        return showInput || captionText != null;
+    }
+
+    void openInput() {
+        showInput = true;
+        inputState = new TextInputState("");
+    }
+
+    void close() {
+        showInput = false;
+        inputState = null;
+        captionText = null;
+        captionFullyTypedTime = 0;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (showInput) {
+            if (ke.isCancel()) {
+                showInput = false;
+                inputState = null;
+            } else if (ke.isConfirm()) {
+                String text = inputState.text().trim();
+                showInput = false;
+                inputState = null;
+                if (!text.isEmpty()) {
+                    captionText = text;
+                    captionStartTime = System.currentTimeMillis();
+                    captionFullyTypedTime = 0;
+                }
+            } else if (ke.isDeleteBackward()) {
+                inputState.deleteBackward();
+            } else if (ke.isDeleteForward()) {
+                inputState.deleteForward();
+            } else if (ke.isLeft()) {
+                inputState.moveCursorLeft();
+            } else if (ke.isRight()) {
+                inputState.moveCursorRight();
+            } else if (ke.isHome()) {
+                inputState.moveCursorToStart();
+            } else if (ke.isEnd()) {
+                inputState.moveCursorToEnd();
+            } else if (ke.code() == KeyCode.CHAR) {
+                inputState.insert(ke.character());
+            }
+            return true;
+        }
+        if (captionText != null) {
+            captionText = null;
+            captionFullyTypedTime = 0;
+            return true;
+        }
+        return false;
+    }
+
+    void tick(long now) {
+        if (captionText == null) {
+            return;
+        }
+        int totalChars = captionText.length();
+        long elapsed = now - captionStartTime;
+        int charsToShow = (int) (elapsed / CHAR_DELAY_MS);
+
+        if (charsToShow >= totalChars && captionFullyTypedTime == 0) {
+            captionFullyTypedTime = now;
+        }
+        if (captionFullyTypedTime > 0 && now - captionFullyTypedTime > 
HOLD_DURATION_MS + FADE_DURATION_MS) {
+            captionText = null;
+            captionFullyTypedTime = 0;
+        }
+    }
+
+    void render(Frame frame, Rect area) {
+        if (showInput) {
+            renderInput(frame, area);
+            return;
+        }
+        if (captionText != null) {
+            renderCaption(frame, area);
+        }
+    }
+
+    void renderFooter(List<Span> spans) {
+        if (showInput) {
+            hint(spans, "Enter", "show");
+            hintLast(spans, "Esc", "cancel");
+        } else if (captionText != null) {
+            hintLast(spans, "any key", "dismiss");
+        }
+    }
+
+    private void renderInput(Frame frame, Rect area) {
+        int popupW = Math.min(50, area.width() - 4);
+        int popupH = 4;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" 💬 Caption ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" show │"),
+                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" cancel "))))
+                .build();
+        frame.renderWidget(block, popup);
+
+        Rect inner = new Rect(popup.left() + 2, popup.top() + 1, popup.width() 
- 4, 1);
+        frame.renderWidget(Paragraph.from(Line.from(
+                Span.styled("Caption text (\\n for newline):", 
Style.EMPTY.dim()))), inner);
+
+        Rect inputArea = new Rect(popup.left() + 2, popup.top() + 2, 
popup.width() - 4, 1);
+        TextInput textInput = TextInput.builder()
+                .cursorStyle(Style.EMPTY.reversed())
+                .build();
+        frame.renderStatefulWidget(textInput, inputArea, inputState);
+    }
+
+    private void renderCaption(Frame frame, Rect area) {
+        long now = System.currentTimeMillis();
+        long elapsed = now - captionStartTime;
+        int charsToShow = Math.min((int) (elapsed / CHAR_DELAY_MS), 
captionText.length());
+        String visible = captionText.substring(0, charsToShow);
+
+        Style style;
+        if (captionFullyTypedTime == 0 || now - captionFullyTypedTime < 
HOLD_DURATION_MS) {
+            style = Style.EMPTY.fg(Color.WHITE).bold();
+        } else {
+            style = Style.EMPTY.dim();
+        }
+
+        String[] parts = visible.split("\\\\n", -1);
+        List<Line> lines = new ArrayList<>();
+        int maxWidth = 0;
+        for (String part : parts) {
+            lines.add(Line.from(Span.styled("  " + part + "  ", style)));
+            maxWidth = Math.max(maxWidth, part.length() + 4);
+        }
+
+        int captionW = Math.min(maxWidth, area.width() - 2);
+        int captionH = lines.size();
+        int captionX = area.left() + Math.max(0, (area.width() - captionW) / 
2);
+        int captionY = area.top() + Math.max(0, (area.height() - captionH) / 
2);
+        Rect captionArea = new Rect(
+                captionX, captionY, Math.min(captionW, area.width()),
+                Math.min(captionH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, captionArea);
+        frame.renderWidget(Paragraph.builder()
+                .text(Text.from(lines.toArray(Line[]::new)))
+                .build(), captionArea);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathPopup.java
new file mode 100644
index 000000000000..6a63320034ec
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathPopup.java
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class ClasspathPopup {
+
+    private static final Style MATCH_STYLE = 
Style.EMPTY.fg(Color.YELLOW).bold();
+
+    private boolean visible;
+    private final ListState listState = new ListState();
+    private final FuzzyFilter fuzzyFilter = new FuzzyFilter();
+    private List<JarEntry> entries;
+    private List<FilteredEntry> filteredEntries;
+    private String baseTitle;
+    private String errorMessage;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open(MonitorContext ctx, String pid, String integrationName) {
+        if (ctx == null || pid == null) {
+            errorMessage = "No integration selected";
+            return;
+        }
+
+        try {
+            Path outputFile = ctx.getOutputFile(pid);
+            Files.deleteIfExists(outputFile);
+            JsonObject action = new JsonObject();
+            action.put("action", "jvm");
+            PathUtils.writeTextSafely(action.toJson(), ctx.getActionFile(pid));
+            JsonObject response = MonitorContext.pollJsonResponse(outputFile, 
5000);
+            if (response != null) {
+                Object cp = response.get("classpath");
+                List<String> paths = new ArrayList<>();
+                if (cp instanceof JsonArray arr) {
+                    for (Object item : arr) {
+                        paths.add(String.valueOf(item));
+                    }
+                } else if (cp instanceof String[] arr) {
+                    for (String s : arr) {
+                        paths.add(s);
+                    }
+                }
+                if (paths.isEmpty()) {
+                    errorMessage = "No classpath information available";
+                    return;
+                }
+                entries = new ArrayList<>();
+                for (String path : paths) {
+                    entries.add(parseJarEntry(path));
+                }
+                entries.sort((a, b) -> 
a.display().compareToIgnoreCase(b.display()));
+                baseTitle = (integrationName != null ? integrationName : pid) 
+ " - Classpath";
+                fuzzyFilter.clearFilter();
+                refilter();
+                listState.select(0);
+                visible = true;
+            } else {
+                errorMessage = "No response from integration";
+            }
+        } catch (Exception e) {
+            errorMessage = "Error fetching classpath: " + e.getMessage();
+        }
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    String consumeError() {
+        String msg = errorMessage;
+        errorMessage = null;
+        return msg;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            if (fuzzyFilter.hasFilter()) {
+                fuzzyFilter.clearFilter();
+                refilter();
+            } else {
+                visible = false;
+            }
+        } else if (ke.isDeleteBackward()) {
+            if (fuzzyFilter.hasFilter()) {
+                fuzzyFilter.deleteChar();
+                refilter();
+            }
+        } else if (ke.isUp()) {
+            listState.selectPrevious();
+        } else if (ke.isDown()) {
+            listState.selectNext(filteredEntries != null ? 
filteredEntries.size() : 0);
+        } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            for (int i = 0; i < 10; i++) {
+                listState.selectPrevious();
+            }
+        } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            if (filteredEntries != null) {
+                for (int i = 0; i < 10; i++) {
+                    listState.selectNext(filteredEntries.size());
+                }
+            }
+        } else if (ke.code() == KeyCode.CHAR) {
+            fuzzyFilter.appendChar(ke.character());
+            refilter();
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (filteredEntries == null) {
+            return;
+        }
+        int itemCount = filteredEntries.size();
+        int popupW = Math.min(100, area.width() - 4);
+        int popupH = Math.min(itemCount + 2, Math.min(30, area.height() - 4));
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        int contentW = popupW - 4;
+        List<ListItem> items = new ArrayList<>();
+        for (FilteredEntry fe : filteredEntries) {
+            String displayText = formatEntry(fe.entry(), contentW);
+            Style normalStyle = fe.entry().isCamel() ? Style.EMPTY : 
Style.EMPTY.dim();
+            if (fe.matchPositions() != null && fe.matchPositions().length > 0) 
{
+                // offset match positions by the formatting prefix (2 spaces)
+                int[] adjusted = adjustPositions(fe.matchPositions(), 
fe.entry(), displayText);
+                Line line = FuzzyFilter.highlightLine(displayText, adjusted, 
normalStyle, MATCH_STYLE);
+                items.add(ListItem.from(Text.from(line)));
+            } else {
+                items.add(ListItem.from(displayText).style(normalStyle));
+            }
+        }
+
+        String title = " " + baseTitle + " (" + filteredEntries.size();
+        if (fuzzyFilter.hasFilter()) {
+            title += "/" + entries.size() + ") [" + fuzzyFilter.filter() + "] 
";
+        } else {
+            title += ") ";
+        }
+
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(title)
+                        .titleBottom(Title.from(Line.from(
+                                Span.styled(" ↑↓", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" navigate │"),
+                                Span.styled(" type", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" filter │"),
+                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE),
+                                Span.raw(fuzzyFilter.hasFilter() ? " clear " : 
" back "))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, listState);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "↑↓", "navigate");
+        hint(spans, "type", "filter");
+        hintLast(spans, "Esc", fuzzyFilter.hasFilter() ? "clear" : "back");
+    }
+
+    private void refilter() {
+        filteredEntries = new ArrayList<>();
+        if (entries == null) {
+            return;
+        }
+        for (JarEntry entry : entries) {
+            if (!fuzzyFilter.hasFilter()) {
+                filteredEntries.add(new FilteredEntry(entry, null));
+            } else {
+                int[] positions = fuzzyFilter.match(entry.display());
+                if (positions != null) {
+                    filteredEntries.add(new FilteredEntry(entry, positions));
+                }
+            }
+        }
+        listState.select(filteredEntries.isEmpty() ? null : 0);
+    }
+
+    private int[] adjustPositions(int[] matchPositions, JarEntry entry, String 
displayText) {
+        // match positions are relative to entry.display() 
(groupId:artifactId:version)
+        // displayText is formatted with "  " prefix and column layout
+        // find where the GAV text starts in the display text
+        String searchText = entry.display();
+        int offset = displayText.indexOf(searchText.substring(0, 
Math.min(searchText.length(), 5)));
+        if (offset < 0) {
+            offset = 2;
+        }
+        int[] adjusted = new int[matchPositions.length];
+        for (int i = 0; i < matchPositions.length; i++) {
+            adjusted[i] = matchPositions[i] + offset;
+        }
+        return adjusted;
+    }
+
+    private String formatEntry(JarEntry entry, int width) {
+        if (entry.groupId() != null) {
+            String gav = entry.groupId() + ":" + entry.artifactId();
+            String ver = entry.version() != null ? entry.version() : "";
+            int gavCol = Math.min(60, width - ver.length() - 4);
+            return String.format("  %-" + gavCol + "s  %s", 
TuiHelper.truncate(gav, gavCol), ver);
+        }
+        return "  " + TuiHelper.truncate(entry.display(), width - 2);
+    }
+
+    static JarEntry parseJarEntry(String path) {
+        // try to extract Maven GAV from path like: 
.m2/repository/org/apache/camel/camel-core/4.x/camel-core-4.x.jar
+        String normalized = path.replace('\\', '/');
+        int repoIdx = normalized.indexOf("/repository/");
+        if (repoIdx >= 0) {
+            String relative = normalized.substring(repoIdx + 
"/repository/".length());
+            int lastSlash = relative.lastIndexOf('/');
+            if (lastSlash > 0) {
+                String parentPath = relative.substring(0, lastSlash);
+                int versionSlash = parentPath.lastIndexOf('/');
+                if (versionSlash > 0) {
+                    String version = parentPath.substring(versionSlash + 1);
+                    String remaining = parentPath.substring(0, versionSlash);
+                    int artifactSlash = remaining.lastIndexOf('/');
+                    if (artifactSlash > 0) {
+                        String artifactId = remaining.substring(artifactSlash 
+ 1);
+                        String groupId = remaining.substring(0, 
artifactSlash).replace('/', '.');
+                        return new JarEntry(groupId, artifactId, version, 
path);
+                    }
+                }
+            }
+        }
+        // fallback: just use filename
+        int slash = normalized.lastIndexOf('/');
+        String filename = slash >= 0 ? normalized.substring(slash + 1) : 
normalized;
+        return new JarEntry(null, filename, null, path);
+    }
+
+    record JarEntry(String groupId, String artifactId, String version, String 
fullPath) {
+        String display() {
+            if (groupId != null) {
+                return groupId + ":" + artifactId + ":" + version;
+            }
+            return fullPath;
+        }
+
+        boolean isCamel() {
+            return groupId != null && groupId.startsWith("org.apache.camel");
+        }
+    }
+
+    record FilteredEntry(JarEntry entry, int[] matchPositions) {
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
new file mode 100644
index 000000000000..865ff567dd89
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.tooling.maven.MavenDownloaderImpl;
+import org.apache.camel.tooling.maven.MavenResolutionException;
+
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class DoctorPopup {
+
+    private boolean visible;
+    private List<Line> lines;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open() {
+        lines = new ArrayList<>();
+        checkJava(lines);
+        checkCamelVersion(lines);
+        checkJBang(lines);
+        checkMavenRepository(lines);
+        checkContainerRuntime(lines);
+        checkCommonPorts(lines);
+        checkDiskSpace(lines);
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (visible) {
+            if (ke.isCancel()) {
+                visible = false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (lines == null || lines.isEmpty()) {
+            return;
+        }
+        int popupW = Math.min(62, area.width() - 4);
+        int popupH = Math.min(lines.size() + 2, area.height() - 4);
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+        Paragraph para = Paragraph.builder()
+                .text(Text.from(lines.toArray(Line[]::new)))
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" 🩺 Doctor ")
+                        .titleBottom(Title.from(Line.from(
+                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+                        .build())
+                .build();
+        frame.renderWidget(para, popup);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hintLast(spans, "Esc", "back");
+    }
+
+    // ---- Checks ----
+
+    private void checkJava(List<Line> result) {
+        String version = System.getProperty("java.version");
+        String vendor = System.getProperty("java.vendor", "");
+        int major = Runtime.version().feature();
+        String status;
+        String emoji;
+        if (major >= 21) {
+            status = null;
+            emoji = "✅";
+        } else if (major >= 17) {
+            status = "Consider upgrading to 21 or 25";
+            emoji = "⚠️";
+        } else {
+            status = "17+ required";
+            emoji = "❌";
+        }
+        result.add(Line.from(
+                Span.raw("  ☕ "),
+                Span.styled(String.format("%-14s", "Java"), 
Style.EMPTY.bold()),
+                Span.raw(String.format("%-30s", version + " (" + vendor + 
")")),
+                Span.raw(" " + emoji)));
+        if (status != null) {
+            result.add(Line.from(Span.styled("                    " + status, 
Style.EMPTY.dim())));
+        }
+    }
+
+    private void checkCamelVersion(List<Line> result) {
+        try {
+            CamelCatalog catalog = new DefaultCamelCatalog();
+            String version = catalog.getCatalogVersion();
+            result.add(Line.from(
+                    Span.raw("  🐪 "),
+                    Span.styled(String.format("%-14s", "Camel"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", version)),
+                    Span.raw(" ✅")));
+        } catch (Exception e) {
+            result.add(Line.from(
+                    Span.raw("  🐪 "),
+                    Span.styled(String.format("%-14s", "Camel"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Not detected")),
+                    Span.raw(" ❌")));
+        }
+    }
+
+    private void checkJBang(List<Line> result) {
+        String version = VersionHelper.getJBangVersion();
+        if (version != null) {
+            result.add(Line.from(
+                    Span.raw("  📦 "),
+                    Span.styled(String.format("%-14s", "JBang"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", version)),
+                    Span.raw(" ✅")));
+        } else {
+            result.add(Line.from(
+                    Span.raw("  📦 "),
+                    Span.styled(String.format("%-14s", "JBang"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Not detected")),
+                    Span.raw(" ⚠️")));
+        }
+    }
+
+    private void checkMavenRepository(List<Line> result) {
+        try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            downloader.build();
+            CamelCatalog catalog = new DefaultCamelCatalog();
+            String version = catalog.getCatalogVersion();
+            downloader.resolveArtifacts(
+                    List.of("org.apache.camel:camel-api:" + version),
+                    Set.of(), false, false);
+            result.add(Line.from(
+                    Span.raw("  🔧 "),
+                    Span.styled(String.format("%-14s", "Maven"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Artifact resolution")),
+                    Span.raw(" ✅")));
+        } catch (MavenResolutionException e) {
+            result.add(Line.from(
+                    Span.raw("  🔧 "),
+                    Span.styled(String.format("%-14s", "Maven"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Resolution failed")),
+                    Span.raw(" ❌")));
+            result.add(Line.from(Span.styled("                    " + 
TuiHelper.truncate(e.getMessage(), 40),
+                    Style.EMPTY.dim())));
+        } catch (Exception e) {
+            result.add(Line.from(
+                    Span.raw("  🔧 "),
+                    Span.styled(String.format("%-14s", "Maven"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Error")),
+                    Span.raw(" ❌")));
+            result.add(Line.from(Span.styled("                    " + 
TuiHelper.truncate(e.getMessage(), 40),
+                    Style.EMPTY.dim())));
+        }
+    }
+
+    private void checkContainerRuntime(List<Line> result) {
+        for (String cmd : new String[] { "docker", "podman" }) {
+            try {
+                Process p = new ProcessBuilder(cmd, "info")
+                        .redirectErrorStream(true)
+                        .start();
+                p.getInputStream().transferTo(OutputStream.nullOutputStream());
+                int exit = p.waitFor();
+                if (exit == 0) {
+                    String name = Character.toUpperCase(cmd.charAt(0)) + 
cmd.substring(1);
+                    result.add(Line.from(
+                            Span.raw("  🐳 "),
+                            Span.styled(String.format("%-14s", "Container"), 
Style.EMPTY.bold()),
+                            Span.raw(String.format("%-30s", name + " 
running")),
+                            Span.raw(" ✅")));
+                    return;
+                }
+            } catch (Exception e) {
+                // not found, try next
+            }
+        }
+        result.add(Line.from(
+                Span.raw("  🐳 "),
+                Span.styled(String.format("%-14s", "Container"), 
Style.EMPTY.bold()),
+                Span.raw(String.format("%-30s", "Not found (optional)")),
+                Span.raw(" ⚠️")));
+    }
+
+    private void checkCommonPorts(List<Line> result) {
+        StringBuilder conflicts = new StringBuilder();
+        for (int port : new int[] { 8080, 8443, 9090 }) {
+            if (isPortInUse(port)) {
+                if (!conflicts.isEmpty()) {
+                    conflicts.append(", ");
+                }
+                conflicts.append(port);
+            }
+        }
+        if (!conflicts.isEmpty()) {
+            result.add(Line.from(
+                    Span.raw("  🔌 "),
+                    Span.styled(String.format("%-14s", "Ports"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "In use: " + conflicts)),
+                    Span.raw(" ⚠️")));
+        } else {
+            result.add(Line.from(
+                    Span.raw("  🔌 "),
+                    Span.styled(String.format("%-14s", "Ports"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "8080, 8443, 9090 free")),
+                    Span.raw(" ✅")));
+        }
+    }
+
+    private static boolean isPortInUse(int port) {
+        try (ServerSocket ss = new ServerSocket(port)) {
+            ss.setReuseAddress(true);
+            return false;
+        } catch (Exception e) {
+            return true;
+        }
+    }
+
+    private void checkDiskSpace(List<Line> result) {
+        File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+        long free = tmpDir.getFreeSpace();
+        long mb = free / (1024 * 1024);
+        long gb = mb / 1024;
+        String emoji = mb > 500 ? "✅" : "⚠️";
+        String unit = gb > 10 ? "GB" : "MB";
+        long value = gb > 0 ? gb : mb;
+        result.add(Line.from(
+                Span.raw("  💾 "),
+                Span.styled(String.format("%-14s", "Disk Space"), 
Style.EMPTY.bold()),
+                Span.raw(String.format("%-30s", value + " " + unit + " free in 
temp dir")),
+                Span.raw(" " + emoji)));
+        if (mb <= 500) {
+            result.add(Line.from(Span.styled("                    Low disk 
space may cause issues",
+                    Style.EMPTY.dim())));
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
new file mode 100644
index 000000000000..067de6c2b617
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+
+/**
+ * Reusable fuzzy filter for TUI list views. Manages filter state and provides 
utilities for fuzzy matching and
+ * highlighted rendering of matched characters.
+ *
+ * <p>
+ * Usage: call {@link #appendChar(char)} / {@link #deleteChar()} as the user 
types. Use {@link #match(String)} to test
+ * each item and get match positions. Use {@link #highlightLine(String, int[], 
Style, Style)} to render with matched
+ * characters highlighted.
+ */
+class FuzzyFilter {
+
+    private String filter = "";
+
+    boolean hasFilter() {
+        return !filter.isEmpty();
+    }
+
+    String filter() {
+        return filter;
+    }
+
+    void appendChar(char c) {
+        filter += Character.toLowerCase(c);
+    }
+
+    void deleteChar() {
+        if (!filter.isEmpty()) {
+            filter = filter.substring(0, filter.length() - 1);
+        }
+    }
+
+    void clearFilter() {
+        filter = "";
+    }
+
+    /**
+     * Fuzzy-match the filter against the given text. Characters in the filter 
must appear in order (but not
+     * consecutively) in the text. Case-insensitive.
+     *
+     * @return positions of matched characters in the text, or null if no match
+     */
+    int[] match(String text) {
+        return fuzzyMatch(text, filter);
+    }
+
+    /**
+     * Static fuzzy match: find each character of {@code pattern} in order 
within {@code text} (case-insensitive).
+     *
+     * @return array of match positions, or null if pattern doesn't match
+     */
+    static int[] fuzzyMatch(String text, String pattern) {
+        if (pattern == null || pattern.isEmpty()) {
+            return new int[0];
+        }
+        String lowerText = text.toLowerCase();
+        int[] positions = new int[pattern.length()];
+        int textIdx = 0;
+        for (int i = 0; i < pattern.length(); i++) {
+            char c = pattern.charAt(i);
+            int found = lowerText.indexOf(c, textIdx);
+            if (found < 0) {
+                return null;
+            }
+            positions[i] = found;
+            textIdx = found + 1;
+        }
+        return positions;
+    }
+
+    /**
+     * Build a {@link Line} with matched character positions highlighted.
+     *
+     * @param  text           the full display text
+     * @param  matchPositions positions of matched characters (from {@link 
#match(String)})
+     * @param  normalStyle    style for non-matched characters
+     * @param  matchStyle     style for matched characters
+     * @return                a Line with interleaved normal and highlighted 
spans
+     */
+    static Line highlightLine(String text, int[] matchPositions, Style 
normalStyle, Style matchStyle) {
+        if (matchPositions == null || matchPositions.length == 0) {
+            return Line.from(Span.styled(text, normalStyle));
+        }
+        List<Span> spans = new ArrayList<>();
+        int pos = 0;
+        for (int matchIdx : matchPositions) {
+            if (matchIdx < pos || matchIdx >= text.length()) {
+                continue;
+            }
+            if (matchIdx > pos) {
+                spans.add(Span.styled(text.substring(pos, matchIdx), 
normalStyle));
+            }
+            spans.add(Span.styled(text.substring(matchIdx, matchIdx + 1), 
matchStyle));
+            pos = matchIdx + 1;
+        }
+        if (pos < text.length()) {
+            spans.add(Span.styled(text.substring(pos), normalStyle));
+        }
+        return Line.from(spans);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
new file mode 100644
index 000000000000..62e12ecdbc20
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -0,0 +1,618 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class RunOptionsForm {
+
+    private static final int PAGE_OPTIONS = 0;
+    private static final int PAGE_PROPERTIES = 1;
+
+    // Row indices for page 0
+    private static final int ROW_NAME = 0;
+    private static final int ROW_PORT = 1;
+    private static final int ROW_MAX = 2;
+    private static final int ROW_DEV = 3;
+    private static final int ROW_OBSERVE = 4;
+    private static final int ROW_TRACE = 5;
+    private static final int ROW_COUNT = 6;
+
+    private boolean visible;
+    private int page;
+    private int selectedRow;
+
+    private static final String[] MAX_MODES = { "Max seconds:", "Max 
messages:", "Max idle secs:" };
+    private static final String[] MAX_FLAGS = { "--max-seconds=", 
"--max-messages=", "--max-idle-seconds=" };
+
+    // Text fields
+    private TextInputState nameInput;
+    private TextInputState portInput;
+    private TextInputState maxInput;
+    private int maxMode;
+
+    // Checkboxes
+    private boolean devMode;
+    private boolean observe;
+    private boolean backlogTrace;
+
+    private String exampleTitle;
+
+    // Properties (page 2)
+    private List<PropertyEntry> properties;
+    private int selectedProperty;
+    private boolean editingKey;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open(String defaultName, String exampleName, boolean bundled) {
+        nameInput = new TextInputState(defaultName != null ? defaultName : "");
+        portInput = new TextInputState("");
+        maxInput = new TextInputState("");
+        maxMode = 0;
+        devMode = false;
+        observe = false;
+        backlogTrace = false;
+        selectedRow = ROW_NAME;
+        page = PAGE_OPTIONS;
+        selectedProperty = 0;
+        exampleTitle = exampleName != null ? exampleName : "Run";
+        loadProperties(exampleName, bundled);
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    String name() {
+        return nameInput != null ? nameInput.text().trim() : "";
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            return true;
+        }
+
+        if (page == PAGE_OPTIONS) {
+            return handleOptionsPage(ke);
+        } else {
+            return handlePropertiesPage(ke);
+        }
+    }
+
+    void render(Frame frame, Rect area) {
+        if (page == PAGE_OPTIONS) {
+            renderOptionsPage(frame, area);
+        } else {
+            renderPropertiesPage(frame, area);
+        }
+    }
+
+    void renderFooter(List<Span> spans) {
+        if (page == PAGE_OPTIONS) {
+            if (hasProperties()) {
+                hint(spans, "Tab", "next");
+            } else {
+                hint(spans, "Tab", "next");
+            }
+            if (selectedRow >= ROW_DEV) {
+                hint(spans, "Space", "toggle");
+            }
+            if (hasProperties()) {
+                hint(spans, "→", "properties");
+            }
+            hint(spans, "Enter", "launch");
+            hintLast(spans, "Esc", "back");
+        } else {
+            hint(spans, "←", "options");
+            hint(spans, "↑↓", "navigate");
+            hint(spans, "+", "add");
+            hint(spans, "Enter", "launch");
+            hintLast(spans, "Esc", "back");
+        }
+    }
+
+    List<String> buildArgs() {
+        List<String> args = new ArrayList<>();
+        String name = nameInput.text().trim();
+        if (!name.isEmpty()) {
+            args.add("--name=" + name);
+        }
+        String port = portInput.text().trim();
+        if (!port.isEmpty()) {
+            args.add("--port=" + port);
+        }
+        String maxVal = maxInput.text().trim();
+        if (!maxVal.isEmpty() && !"0".equals(maxVal)) {
+            args.add(MAX_FLAGS[maxMode] + maxVal);
+        }
+        if (devMode) {
+            args.add("--dev");
+        }
+        if (observe) {
+            args.add("--observe");
+        }
+        if (backlogTrace) {
+            args.add("--backlog-trace");
+        }
+        if (properties != null) {
+            for (PropertyEntry pe : properties) {
+                String current = pe.valueInput().text();
+                String key = pe.effectiveKey();
+                if (key.isEmpty()) {
+                    continue;
+                }
+                if (pe.isCustom() || !current.equals(pe.originalValue())) {
+                    args.add("--prop=" + key + "=" + current);
+                }
+            }
+        }
+        return args;
+    }
+
+    // ---- Options page (page 0) ----
+
+    private boolean handleOptionsPage(KeyEvent ke) {
+        if (ke.isUp()) {
+            selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
+            return true;
+        }
+        if (ke.isDown()) {
+            if (selectedRow == ROW_TRACE && hasProperties()) {
+                page = PAGE_PROPERTIES;
+                selectedProperty = 0;
+            } else {
+                selectedRow = (selectedRow + 1) % ROW_COUNT;
+            }
+            return true;
+        }
+        if (ke.isFocusNext()) {
+            if (selectedRow == ROW_TRACE && hasProperties()) {
+                page = PAGE_PROPERTIES;
+                selectedProperty = 0;
+            } else {
+                selectedRow = (selectedRow + 1) % ROW_COUNT;
+            }
+            return true;
+        }
+        if (ke.isFocusPrevious()) {
+            selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
+            return true;
+        }
+        if (ke.isRight() && hasProperties() && selectedRow >= ROW_DEV) {
+            page = PAGE_PROPERTIES;
+            selectedProperty = 0;
+            return true;
+        }
+
+        if (ke.isChar('+') && selectedRow >= ROW_DEV) {
+            page = PAGE_PROPERTIES;
+            addCustomProperty();
+            return true;
+        }
+
+        // Max row: Space cycles mode
+        if (ke.isChar(' ') && selectedRow == ROW_MAX) {
+            maxMode = (maxMode + 1) % MAX_MODES.length;
+            return true;
+        }
+
+        // Checkbox rows: Space toggles
+        if (ke.isChar(' ') && selectedRow >= ROW_DEV) {
+            switch (selectedRow) {
+                case ROW_DEV -> devMode = !devMode;
+                case ROW_OBSERVE -> observe = !observe;
+                case ROW_TRACE -> backlogTrace = !backlogTrace;
+            }
+            return true;
+        }
+
+        // Text field rows: delegate to active input
+        if (selectedRow <= ROW_MAX) {
+            TextInputState active = activeInput();
+            if (active != null) {
+                handleTextInput(ke, active, selectedRow == ROW_PORT || 
selectedRow == ROW_MAX);
+            }
+            return true;
+        }
+        return true;
+    }
+
+    // ---- Properties page (page 1) ----
+
+    private boolean handlePropertiesPage(KeyEvent ke) {
+        if (ke.isChar('+')) {
+            addCustomProperty();
+            return true;
+        }
+        if (ke.isUp()) {
+            editingKey = false;
+            if (selectedProperty == 0) {
+                page = PAGE_OPTIONS;
+                selectedRow = ROW_TRACE;
+            } else {
+                selectedProperty--;
+            }
+            return true;
+        }
+        if (ke.isDown()) {
+            editingKey = false;
+            if (selectedProperty < properties.size() - 1) {
+                selectedProperty++;
+            }
+            return true;
+        }
+        if (ke.isFocusNext()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            if (current.isCustom() && editingKey) {
+                editingKey = false;
+            } else if (selectedProperty < properties.size() - 1) {
+                editingKey = false;
+                selectedProperty++;
+            }
+            return true;
+        }
+        if (ke.isFocusPrevious()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            if (current.isCustom() && !editingKey) {
+                editingKey = true;
+            } else if (selectedProperty == 0) {
+                editingKey = false;
+                page = PAGE_OPTIONS;
+                selectedRow = ROW_TRACE;
+            } else {
+                editingKey = false;
+                selectedProperty--;
+            }
+            return true;
+        }
+        if (ke.isLeft()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            TextInputState active = editingKey ? current.keyInput() : 
current.valueInput();
+            if (active.cursorPosition() == 0) {
+                if (current.isCustom() && !editingKey) {
+                    editingKey = true;
+                    return true;
+                }
+                page = PAGE_OPTIONS;
+                selectedRow = ROW_TRACE;
+                editingKey = false;
+                return true;
+            }
+        }
+        if (ke.isDeleteBackward()) {
+            PropertyEntry current = properties.get(selectedProperty);
+            if (current.isCustom() && editingKey) {
+                if (current.keyInput().text().isEmpty()) {
+                    properties.remove(selectedProperty);
+                    if (selectedProperty >= properties.size() && 
selectedProperty > 0) {
+                        selectedProperty--;
+                    }
+                    if (properties.isEmpty()) {
+                        page = PAGE_OPTIONS;
+                        selectedRow = ROW_TRACE;
+                    }
+                    return true;
+                }
+            }
+        }
+
+        // Text editing for selected property
+        PropertyEntry current = properties.get(selectedProperty);
+        TextInputState active = (current.isCustom() && editingKey) ? 
current.keyInput() : current.valueInput();
+        handleTextInput(ke, active, false);
+        return true;
+    }
+
+    private void addCustomProperty() {
+        if (properties == null) {
+            properties = new ArrayList<>();
+        }
+        PropertyEntry entry = new PropertyEntry("", "", new 
TextInputState(""), new TextInputState(""));
+        properties.add(entry);
+        selectedProperty = properties.size() - 1;
+        editingKey = true;
+    }
+
+    // ---- Rendering ----
+
+    private void renderOptionsPage(Frame frame, Rect area) {
+        int popupW = Math.min(56, area.width() - 4);
+        int popupH = 10;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        String title = " Run: " + exampleTitle;
+        if (hasProperties()) {
+            title += " (1/2) ";
+        } else {
+            title += " ";
+        }
+
+        List<Span> bottomSpans = new ArrayList<>();
+        bottomSpans.add(Span.styled(" Tab", MonitorContext.HINT_KEY_STYLE));
+        bottomSpans.add(Span.raw(" next"));
+        if (hasProperties()) {
+            bottomSpans.add(Span.raw(" │"));
+            bottomSpans.add(Span.styled(" →", MonitorContext.HINT_KEY_STYLE));
+            bottomSpans.add(Span.raw(" properties"));
+        }
+        bottomSpans.add(Span.raw(" │"));
+        bottomSpans.add(Span.styled(" Space", MonitorContext.HINT_KEY_STYLE));
+        bottomSpans.add(Span.raw(" toggle │"));
+        bottomSpans.add(Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE));
+        bottomSpans.add(Span.raw(" launch │"));
+        bottomSpans.add(Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE));
+        bottomSpans.add(Span.raw(" back "));
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(title)
+                .titleBottom(Title.from(Line.from(bottomSpans)))
+                .build();
+        frame.renderWidget(block, popup);
+
+        int innerX = popup.left() + 2;
+        int innerW = popup.width() - 4;
+        int labelW = 16;
+        int fieldW = innerW - labelW;
+        int rowY = popup.top() + 1;
+
+        renderLabel(frame, innerX, rowY, labelW, "Name:", selectedRow == 
ROW_NAME);
+        renderTextInput(frame, innerX + labelW, rowY, fieldW, nameInput, 
selectedRow == ROW_NAME);
+        rowY++;
+
+        renderLabel(frame, innerX, rowY, labelW, "Port:", selectedRow == 
ROW_PORT);
+        renderTextInput(frame, innerX + labelW, rowY, fieldW, portInput, 
selectedRow == ROW_PORT);
+        rowY++;
+
+        renderLabel(frame, innerX, rowY, labelW, MAX_MODES[maxMode], 
selectedRow == ROW_MAX);
+        renderTextInput(frame, innerX + labelW, rowY, fieldW, maxInput, 
selectedRow == ROW_MAX);
+        rowY++;
+
+        renderCheckbox(frame, innerX, rowY, innerW, "Dev mode (live reload)", 
devMode, selectedRow == ROW_DEV);
+        rowY++;
+
+        renderCheckbox(frame, innerX, rowY, innerW, "Observe (health + 
metrics)", observe, selectedRow == ROW_OBSERVE);
+        rowY++;
+
+        renderCheckbox(frame, innerX, rowY, innerW, "Backlog trace", 
backlogTrace, selectedRow == ROW_TRACE);
+    }
+
+    private void renderPropertiesPage(Frame frame, Rect area) {
+        int popupW = Math.min(70, area.width() - 4);
+        int propCount = properties != null ? properties.size() : 0;
+        int popupH = Math.min(propCount + 2, Math.min(20, area.height() - 4));
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Run: " + exampleTitle + " — Properties (2/2) ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" ←", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" options │"),
+                        Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" navigate │"),
+                        Span.styled(" +", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" add │"),
+                        Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" launch │"),
+                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
+                .build();
+        frame.renderWidget(block, popup);
+
+        int innerX = popup.left() + 2;
+        int innerW = popup.width() - 4;
+        int maxKeyLen = 0;
+        for (PropertyEntry pe : properties) {
+            maxKeyLen = Math.max(maxKeyLen, pe.key().length());
+        }
+        int labelW = Math.min(maxKeyLen + 2, innerW / 2);
+        int fieldW = innerW - labelW;
+
+        int rowY = popup.top() + 1;
+        int visibleRows = popup.height() - 2;
+        int scrollOffset = Math.max(0, selectedProperty - visibleRows + 1);
+
+        for (int i = scrollOffset; i < properties.size() && (rowY - 
popup.top() - 1) < visibleRows; i++) {
+            PropertyEntry pe = properties.get(i);
+            boolean selected = (i == selectedProperty);
+
+            if (pe.isCustom()) {
+                // Custom entry: editable key and value
+                if (selected && editingKey) {
+                    renderTextInput(frame, innerX, rowY, labelW - 1, 
pe.keyInput(), true);
+                } else {
+                    String keyText = pe.keyInput().text();
+                    Style keyStyle = selected ? Style.EMPTY.bold() : 
(keyText.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY);
+                    Rect keyArea = new Rect(innerX, rowY, labelW - 1, 1);
+                    frame.renderWidget(Paragraph.from(Line.from(
+                            Span.styled(keyText.isEmpty() ? "<key>" : keyText, 
keyStyle))), keyArea);
+                }
+                Rect colonArea = new Rect(innerX + labelW - 1, rowY, 1, 1);
+                frame.renderWidget(Paragraph.from(Line.from(Span.styled(":", 
Style.EMPTY.dim()))), colonArea);
+                if (selected && !editingKey) {
+                    renderTextInput(frame, innerX + labelW, rowY, fieldW, 
pe.valueInput(), true);
+                } else {
+                    String text = pe.valueInput().text();
+                    Style style = text.isEmpty() ? Style.EMPTY.dim() : 
Style.EMPTY;
+                    Rect valArea = new Rect(innerX + labelW, rowY, fieldW, 1);
+                    frame.renderWidget(Paragraph.from(Line.from(
+                            Span.styled(text.isEmpty() ? "<value>" : text, 
style))), valArea);
+                }
+            } else {
+                // Loaded entry: fixed key, editable value
+                String keyLabel = pe.key() + ":";
+                renderLabel(frame, innerX, rowY, labelW, 
TuiHelper.truncate(keyLabel, labelW), selected);
+
+                boolean modified = 
!pe.valueInput().text().equals(pe.originalValue());
+                if (selected) {
+                    renderTextInput(frame, innerX + labelW, rowY, fieldW, 
pe.valueInput(), true);
+                } else {
+                    String text = pe.valueInput().text();
+                    Style style = modified ? Style.EMPTY.bold() : Style.EMPTY;
+                    Rect inputArea = new Rect(innerX + labelW, rowY, fieldW, 
1);
+                    frame.renderWidget(Paragraph.from(Line.from(
+                            Span.styled(text.isEmpty() ? "—" : text, style))), 
inputArea);
+                }
+            }
+            rowY++;
+        }
+    }
+
+    // ---- Properties loading ----
+
+    private void loadProperties(String exampleName, boolean bundled) {
+        properties = new ArrayList<>();
+        if (exampleName == null || exampleName.isEmpty()) {
+            return;
+        }
+        String content;
+        if (bundled) {
+            content = DocHelper.loadResourceContent("examples/" + exampleName 
+ "/application.properties");
+        } else {
+            content = DocHelper.downloadContent(
+                    
"https://raw.githubusercontent.com/apache/camel-jbang-examples/main/";
+                                                + exampleName + 
"/application.properties");
+        }
+        if (content == null || content.isBlank()) {
+            return;
+        }
+        for (String line : content.split("\n")) {
+            String trimmed = line.trim();
+            if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+                continue;
+            }
+            int eq = trimmed.indexOf('=');
+            if (eq > 0) {
+                String key = trimmed.substring(0, eq).trim();
+                String value = trimmed.substring(eq + 1).trim();
+                properties.add(new PropertyEntry(key, value, null, new 
TextInputState(value)));
+            }
+        }
+    }
+
+    private boolean hasProperties() {
+        return properties != null && !properties.isEmpty();
+    }
+
+    // ---- Shared helpers ----
+
+    private TextInputState activeInput() {
+        return switch (selectedRow) {
+            case ROW_NAME -> nameInput;
+            case ROW_PORT -> portInput;
+            case ROW_MAX -> maxInput;
+            default -> null;
+        };
+    }
+
+    private void handleTextInput(KeyEvent ke, TextInputState active, boolean 
digitsOnly) {
+        if (ke.isDeleteBackward()) {
+            active.deleteBackward();
+        } else if (ke.isDeleteForward()) {
+            active.deleteForward();
+        } else if (ke.isLeft()) {
+            active.moveCursorLeft();
+        } else if (ke.isRight()) {
+            active.moveCursorRight();
+        } else if (ke.isHome()) {
+            active.moveCursorToStart();
+        } else if (ke.isEnd()) {
+            active.moveCursorToEnd();
+        } else if (ke.code() == KeyCode.CHAR) {
+            if (digitsOnly) {
+                if (Character.isDigit(ke.character())) {
+                    active.insert(ke.character());
+                }
+            } else {
+                active.insert(ke.character());
+            }
+        }
+    }
+
+    private void renderLabel(Frame frame, int x, int y, int w, String label, 
boolean selected) {
+        Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim();
+        Rect labelArea = new Rect(x, y, w, 1);
+        frame.renderWidget(Paragraph.from(Line.from(Span.styled(label, 
style))), labelArea);
+    }
+
+    private void renderTextInput(Frame frame, int x, int y, int w, 
TextInputState state, boolean active) {
+        Rect inputArea = new Rect(x, y, w, 1);
+        if (active) {
+            TextInput textInput = TextInput.builder()
+                    .cursorStyle(Style.EMPTY.reversed())
+                    .build();
+            frame.renderStatefulWidget(textInput, inputArea, state);
+        } else {
+            String text = state.text();
+            Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(text.isEmpty() ? "—" : text, style))), 
inputArea);
+        }
+    }
+
+    private void renderCheckbox(Frame frame, int x, int y, int w, String 
label, boolean checked, boolean selected) {
+        String box = checked ? "[x]" : "[ ]";
+        Style style = selected ? Style.EMPTY.bold().reversed() : Style.EMPTY;
+        Rect cbArea = new Rect(x, y, w, 1);
+        frame.renderWidget(Paragraph.from(Line.from(
+                Span.styled(" " + box + " " + label, style))), cbArea);
+    }
+
+    record PropertyEntry(String key, String originalValue, TextInputState 
keyInput, TextInputState valueInput) {
+        boolean isCustom() {
+            return keyInput != null;
+        }
+
+        String effectiveKey() {
+            return keyInput != null ? keyInput.text().trim() : key;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java
new file mode 100644
index 000000000000..bbf7d69750de
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.function.Supplier;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class StopAllPopup {
+
+    private final Supplier<List<IntegrationInfo>> integrations;
+    private final Supplier<List<InfraInfo>> infraServices;
+
+    private boolean visible;
+    private boolean checkIntegrations = true;
+    private boolean checkInfra = true;
+    private int selectedRow;
+    private int integrationCount;
+    private int infraCount;
+
+    private String notification;
+
+    StopAllPopup(Supplier<List<IntegrationInfo>> integrations, 
Supplier<List<InfraInfo>> infraServices) {
+        this.integrations = integrations;
+        this.infraServices = infraServices;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    boolean hasBothGroups() {
+        List<IntegrationInfo> ints = integrations.get();
+        List<InfraInfo> infras = infraServices.get();
+        long ic = ints.stream().filter(i -> !i.vanishing).count();
+        long fc = infras.stream().filter(i -> !i.vanishing).count();
+        return ic > 0 && fc > 0;
+    }
+
+    void open() {
+        List<IntegrationInfo> ints = integrations.get();
+        List<InfraInfo> infras = infraServices.get();
+        integrationCount = (int) ints.stream().filter(i -> 
!i.vanishing).count();
+        infraCount = (int) infras.stream().filter(i -> !i.vanishing).count();
+
+        if (integrationCount == 0 && infraCount == 0) {
+            notification = "No running processes to stop";
+            return;
+        }
+
+        if (integrationCount > 0 && infraCount == 0) {
+            stopIntegrations();
+            return;
+        }
+        if (infraCount > 0 && integrationCount == 0) {
+            stopInfraServices();
+            return;
+        }
+
+        checkIntegrations = true;
+        checkInfra = true;
+        selectedRow = 0;
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    String consumeNotification() {
+        String msg = notification;
+        notification = null;
+        return msg;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+        } else if (ke.isUp()) {
+            selectedRow = 0;
+        } else if (ke.isDown()) {
+            selectedRow = 1;
+        } else if (ke.isChar(' ')) {
+            if (selectedRow == 0) {
+                checkIntegrations = !checkIntegrations;
+            } else {
+                checkInfra = !checkInfra;
+            }
+        } else if (ke.isConfirm()) {
+            visible = false;
+            executeStop();
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        int popupW = Math.min(42, area.width() - 4);
+        int popupH = 6;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        String intLabel = (checkIntegrations ? "[x]" : "[ ]") + " All 
integrations (" + integrationCount + ")";
+        String infraLabel = (checkInfra ? "[x]" : "[ ]") + " All infra 
services (" + infraCount + ")";
+
+        Style normalStyle = Style.EMPTY;
+        Style selectedStyle = Style.EMPTY.bold().reversed();
+
+        Line intLine = Line.from(Span.styled("  " + intLabel, selectedRow == 0 
? selectedStyle : normalStyle));
+        Line infraLine = Line.from(Span.styled("  " + infraLabel, selectedRow 
== 1 ? selectedStyle : normalStyle));
+
+        Paragraph para = Paragraph.builder()
+                .text(Text.from(Line.from(""), intLine, infraLine))
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" 🛑 Stop All ")
+                        .titleBottom(Title.from(Line.from(
+                                Span.styled(" Space", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" toggle │"),
+                                Span.styled(" Enter", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" confirm │"),
+                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" cancel "))))
+                        .build())
+                .build();
+        frame.renderWidget(para, popup);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "Space", "toggle");
+        hint(spans, "Enter", "confirm");
+        hintLast(spans, "Esc", "cancel");
+    }
+
+    private void executeStop() {
+        int stoppedInt = 0;
+        int stoppedInfra = 0;
+        if (checkIntegrations) {
+            stoppedInt = stopIntegrations();
+        }
+        if (checkInfra) {
+            stoppedInfra = stopInfraServices();
+        }
+        if (stoppedInt == 0 && stoppedInfra == 0 && notification == null) {
+            notification = "Nothing selected to stop";
+        }
+    }
+
+    private int stopIntegrations() {
+        List<IntegrationInfo> ints = integrations.get();
+        int count = 0;
+        for (IntegrationInfo info : ints) {
+            if (info.vanishing || info.pid == null) {
+                continue;
+            }
+            try {
+                long pid = Long.parseLong(info.pid);
+                ProcessHandle.of(pid).ifPresent(ProcessHandle::destroy);
+                count++;
+            } catch (NumberFormatException e) {
+                // skip
+            }
+        }
+        if (count > 0) {
+            notification = "Stopping " + count + " integration" + (count > 1 ? 
"s" : "");
+        }
+        return count;
+    }
+
+    private int stopInfraServices() {
+        List<InfraInfo> infras = infraServices.get();
+        Path camelDir = CommandLineHelper.getCamelDir();
+        int count = 0;
+        for (InfraInfo info : infras) {
+            if (info.vanishing || info.pid == null) {
+                continue;
+            }
+            PathUtils.deleteFile(camelDir.resolve("infra-" + info.alias + "-" 
+ info.pid + ".json"));
+            try {
+                long pid = Long.parseLong(info.pid);
+                ProcessHandle.of(pid).ifPresent(ProcessHandle::destroy);
+                count++;
+            } catch (NumberFormatException e) {
+                // skip
+            }
+        }
+        if (count > 0) {
+            String prev = notification;
+            String msg = "Stopping " + count + " infra service" + (count > 1 ? 
"s" : "");
+            notification = prev != null ? prev + " and " + msg.substring(0, 
1).toLowerCase() + msg.substring(1) : msg;
+        }
+        return count;
+    }
+}

Reply via email to