You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/01 15:50:29 UTC

(camel) branch quick-fix/run-and-quit created (now 90107b56793a)

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

davsclaus pushed a change to branch quick-fix/run-and-quit
in repository https://gitbox.apache.org/repos/asf/camel.git


      at 90107b56793a chore: camel-jbang - the run form warns about a taken port, and quitting says which integrations started here keep running

This branch includes the following new commits:

     new 90107b56793a chore: camel-jbang - the run form warns about a taken port, and quitting says which integrations started here keep running

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: chore: camel-jbang - the run form warns about a taken port, and quitting says which integrations started here keep running

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch quick-fix/run-and-quit
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 90107b56793ada8ce3419c47740f5721fcb0d9a7
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Oct 1 17:11:54 2026 +0200

    chore: camel-jbang - the run form warns about a taken port, and quitting says which integrations started here keep running
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../ROOT/pages/camel-jbang-tui-settings.adoc       |  2 +-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  3 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  1 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 20 +++++-
 .../dsl/jbang/core/commands/tui/DialogHelper.java  | 38 ++++++++++
 .../dsl/jbang/core/commands/tui/LaunchManager.java | 18 +++++
 .../dsl/jbang/core/commands/tui/PopupManager.java  | 42 ++++++++++-
 .../dsl/jbang/core/commands/tui/PortCheck.java     | 83 ++++++++++++++++++++++
 .../jbang/core/commands/tui/RunOptionsForm.java    | 41 ++++++++++-
 .../jbang/core/commands/tui/DialogHelperTest.java  | 17 +++++
 .../jbang/core/commands/tui/PopupManagerTest.java  | 23 ++++++
 .../dsl/jbang/core/commands/tui/PortCheckTest.java | 62 ++++++++++++++++
 12 files changed, 344 insertions(+), 6 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
index 96e3ab5bcba1..85e17f2d1bc5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
@@ -35,7 +35,7 @@ Press *F2* to open the actions menu with quick access to common operations:
 | Settings... | Change the theme, the starting tab, and the default run-from-folder.
 | Take Screenshot | Export the current screen as SVG, text, or ANSI art.
 | Start/Stop Tape Recording | Record your session as a `.tape` file for demos.
-| Quit | Quit the TUI, with the same confirmation as pressing *q*. The last entry of the menu, so quitting is one menu away on tabs and in input fields where *q* is taken by something else.
+| Quit | Quit the TUI, with the same confirmation as pressing *q*. The last entry of the menu, so quitting is one menu away on tabs and in input fields where *q* is taken by something else. Integrations started from the TUI keep running after it quits (until the terminal closes); when some do, the confirmation says how many, and *s* stops them and quits.
 |===
 
 === Sending Test Messages
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 71a2c91b5205..c124ccdd6e76 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -68,7 +68,8 @@ Spring Boot, Quarkus, or JBang. This makes it easy to try any example on all thr
 changing a single line of code. The first three run the example in a separate JVM that only
 contains the dependencies of the example (like a production deployment), while JBang runs it
 in-process in the Camel CLI JVM, which starts faster but has the CLI on the classpath as well. You can also set the integration name, toggle dev mode, and
-add extra dependencies.
+add extra dependencies. When the port the app will listen on (the one you set, else 8080) is taken already, the form
+says by whom, so a second app with an HTTP server does not fail to start.
 
 The example starts running in the background. The TUI auto-selects it as soon as it appears.
 From there you can explore tabs, watch messages flow, inspect the route diagram, and experiment.
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 9b0884f339cb..e648baf0beba 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
@@ -144,6 +144,7 @@ class ActionsPopup {
                  Runnable burstCallback, Set<String> stoppingPids) {
         this.runningNames = runningNames;
         this.integrations = integrations;
+        runOptionsForm.setIntegrations(integrations);
         this.infraServices = infraServices;
         this.captionOverlay = captionOverlay;
         this.screenshotAction = screenshotAction;
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 32fec82f9560..57f648a60b54 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
@@ -533,7 +533,21 @@ public class CamelMonitor extends CamelCommand {
      */
     void quitTui(boolean confirm) {
         if (confirm && ctx.confirmActions) {
-            popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> runner.quit());
+            LaunchManager launches = actionsPopup.getLaunchManager();
+            long running = launches.runningLaunchCount();
+            if (running > 0) {
+                // what was started from here keeps running when the TUI quits: say so, and offer to stop it
+                String what = running == 1
+                        ? "1 integration started here keeps running"
+                        : running + " integrations started here keep running";
+                popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", what, () -> runner.quit(),
+                        's', running == 1 ? "stop it and quit" : "stop them and quit", () -> {
+                            launches.stopLaunched();
+                            runner.quit();
+                        });
+            } else {
+                popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> runner.quit());
+            }
         } else {
             runner.quit();
         }
@@ -2772,6 +2786,10 @@ public class CamelMonitor extends CamelCommand {
             filesBrowser.renderFooter(spans);
         } else if (popupManager.isKillConfirmVisible() || popupManager.isConfirmVisible()) {
             hint(spans, "Enter", "confirm");
+            String extra = popupManager.isConfirmVisible() ? popupManager.confirmExtraHint() : null;
+            if (extra != null) {
+                hint(spans, extra.substring(0, 1), extra.substring(2));
+            }
             hintLast(spans, "Esc", "cancel");
         } else if (popupManager.isSwitchPopupVisible()) {
             hint(spans, "Enter", "switch");
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
index afd8acd1d076..d645a2ac92a7 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
@@ -16,6 +16,9 @@
  */
 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;
@@ -127,6 +130,41 @@ final class DialogHelper {
         return popup;
     }
 
+    /**
+     * Renders a confirm dialog with a line that tells more and, when {@code extraKey} is given, a second choice on its
+     * own key between Enter and Esc.
+     */
+    static Rect renderConfirm(
+            Frame frame, Rect area, String title, String message, String detail, String extraKey, String extraLabel) {
+        Style accent = Theme.warning();
+        String titleText = " " + title + " ";
+        String msg = message.trim();
+        Line hints = extraKey != null
+                ? TuiHelper.hintLine("Enter", "confirm", extraKey, extraLabel, "Esc", "cancel")
+                : TuiHelper.hintLine("Enter", "confirm", "Esc", "cancel");
+        int contentW = Math.max(msg.length(), Math.max(detail != null ? detail.length() : 0, hints.width()));
+        int popupW = clampWidth(area, CONFIRM_MIN_WIDTH, Math.max(contentW + 6, titleText.length() + 4));
+        Rect popup = centered(area, popupW, CONFIRM_HEIGHT + (detail != null ? 1 : 0));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .borderStyle(accent)
+                .title(Title.from(Line.from(Span.styled(titleText, accent.bold()))))
+                .build();
+        frame.renderWidget(block, popup);
+        List<Line> lines = new ArrayList<>();
+        lines.add(Line.empty());
+        lines.add(Line.from(Span.styled(msg, accent.bold())));
+        if (detail != null) {
+            lines.add(Line.from(Span.styled(detail, Theme.muted())));
+        }
+        lines.add(Line.empty());
+        lines.add(hints);
+        frame.renderWidget(Paragraph.builder().centered().text(Text.from(lines)).build(), block.inner(popup));
+        return popup;
+    }
+
     /**
      * Renders a dialog with a single text field, e.g. for entering a new file name.
      *
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
index c169ad862787..f5502aaf9e51 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
@@ -43,6 +43,8 @@ class LaunchManager {
     private final Supplier<List<InfraInfo>> infraServices;
     // added from the UI thread and from tool threads (MCP, the example launcher), read on the UI thread
     private final Queue<PendingLaunch> pendingLaunches = new ConcurrentLinkedQueue<>();
+    // integrations started from the TUI (not infra services, which run in the background on their own)
+    private final Queue<Process> launched = new ConcurrentLinkedQueue<>();
     private DeferredLaunch deferredLaunch;
     private volatile String pendingAutoSelect;
     private BiConsumer<String, Boolean> notificationCallback;
@@ -161,6 +163,22 @@ class LaunchManager {
     void addPendingLaunch(String name, Process process, Path outputFile) {
         pendingLaunches.add(new PendingLaunch(name, process, outputFile, System.currentTimeMillis()));
         pendingAutoSelect = name;
+        launched.add(process);
+    }
+
+    /** The integrations started from this TUI that still run: they keep running when the TUI quits. */
+    long runningLaunchCount() {
+        launched.removeIf(p -> !p.isAlive());
+        return launched.size();
+    }
+
+    /** Stops the integrations started from this TUI (the camel launcher and the JVM it started). */
+    void stopLaunched() {
+        for (Process p : launched) {
+            p.descendants().forEach(ProcessHandle::destroy);
+            p.destroy();
+        }
+        launched.clear();
     }
 
     void addPendingLaunchNoAutoSelect(String name, Process process, Path outputFile) {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index 600bb1a35a24..544753f51a92 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -90,6 +90,10 @@ class PopupManager {
     private String confirmMessage;
     private Runnable confirmCallback;
     private Runnable cancelCallback;
+    private String confirmDetail;
+    private char confirmExtraKey;
+    private String confirmExtraLabel;
+    private Runnable confirmExtraCallback;
 
     // Last rendered popup rects for mouse hit-testing
     private Rect lastMorePopupRect;
@@ -176,6 +180,11 @@ class PopupManager {
         return showKillConfirm;
     }
 
+    /** The second choice of the confirm dialog as "key label", or null when it has none. */
+    String confirmExtraHint() {
+        return confirmExtraCallback != null ? confirmExtraKey + " " + confirmExtraLabel : null;
+    }
+
     boolean isConfirmVisible() {
         return showConfirm;
     }
@@ -245,9 +254,26 @@ class PopupManager {
         this.confirmMessage = message;
         this.confirmCallback = onConfirm;
         this.cancelCallback = onCancel;
+        this.confirmDetail = null;
+        this.confirmExtraLabel = null;
+        this.confirmExtraCallback = null;
         this.showConfirm = true;
     }
 
+    /**
+     * As {@link #showConfirm(String, String, Runnable)}, with a line that tells more, and a second choice on its own
+     * key (for example quit, or stop what was started and quit).
+     */
+    void showConfirm(
+            String title, String message, String detail, Runnable onConfirm, char extraKey, String extraLabel,
+            Runnable onExtra) {
+        showConfirm(title, message, onConfirm, null);
+        this.confirmDetail = detail;
+        this.confirmExtraKey = extraKey;
+        this.confirmExtraLabel = extraLabel;
+        this.confirmExtraCallback = onExtra;
+    }
+
     /** Closes the generic confirm dialog without running either callback (for example on a timeout). */
     void dismissConfirm() {
         showConfirm = false;
@@ -437,7 +463,14 @@ class PopupManager {
     }
 
     private boolean handleConfirmKeys(KeyEvent ke) {
-        if (ke.isConfirm()) {
+        if (confirmExtraCallback != null && ke.isChar(confirmExtraKey)) {
+            showConfirm = false;
+            Runnable cb = confirmExtraCallback;
+            confirmCallback = null;
+            cancelCallback = null;
+            confirmExtraCallback = null;
+            cb.run();
+        } else if (ke.isConfirm()) {
             showConfirm = false;
             Runnable cb = confirmCallback;
             confirmCallback = null;
@@ -724,7 +757,12 @@ class PopupManager {
     void renderConfirm(Frame frame, Rect area) {
         String msg = confirmMessage != null ? confirmMessage : "";
         String title = confirmTitle != null ? confirmTitle : "Confirm";
-        DialogHelper.renderConfirm(frame, area, title, msg, false);
+        if (confirmDetail != null || confirmExtraCallback != null) {
+            DialogHelper.renderConfirm(frame, area, title, msg, confirmDetail,
+                    confirmExtraCallback != null ? String.valueOf(confirmExtraKey) : null, confirmExtraLabel);
+        } else {
+            DialogHelper.renderConfirm(frame, area, title, msg, false);
+        }
     }
 
     int[] morePopupShortcut(KeyEvent ke) {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java
new file mode 100644
index 000000000000..2cfbf7092757
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheck.java
@@ -0,0 +1,83 @@
+/*
+ * 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.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * Whether the HTTP port of an app about to be run is taken already, and by whom: a Spring Boot app then fails with
+ * APPLICATION FAILED TO START, and a Camel app cannot start its HTTP server.
+ */
+final class PortCheck {
+
+    /** The port an app listens on when none is given (Camel, Spring Boot and Quarkus alike). */
+    static final int DEFAULT_PORT = 8080;
+
+    private PortCheck() {
+    }
+
+    /** Whether something listens on the port already. */
+    static boolean inUse(int port) {
+        if (port <= 0) {
+            return false;
+        }
+        try (ServerSocket socket = new ServerSocket()) {
+            socket.setReuseAddress(false);
+            socket.bind(new InetSocketAddress(port));
+            return false;
+        } catch (IOException e) {
+            return true;
+        }
+    }
+
+    /** The integration that serves HTTP on the port, from what the TUI knows of the running ones; null if none. */
+    static String owner(int port, List<IntegrationInfo> integrations) {
+        for (IntegrationInfo info : integrations) {
+            if (info.vanishing || info.phantom) {
+                continue;
+            }
+            for (HttpEndpointInfo ep : info.httpEndpoints) {
+                if (ep.url != null && portOf(ep.url) == port) {
+                    return info.name != null ? info.name : info.pid;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** The warning for the run form, or null when the port is free. */
+    static String warning(int port, List<IntegrationInfo> integrations) {
+        if (!inUse(port)) {
+            return null;
+        }
+        String owner = owner(port, integrations);
+        return "Port " + port + " is taken by " + (owner != null ? owner : "another process")
+               + "; set another if this app serves HTTP";
+    }
+
+    private static int portOf(String url) {
+        try {
+            return URI.create(url).getPort();
+        } catch (IllegalArgumentException e) {
+            return -1;
+        }
+    }
+}
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
index b463e69d6f4e..f2d3f3f35c71 100644
--- 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
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Supplier;
 
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
@@ -65,6 +66,11 @@ class RunOptionsForm {
     private int page;
     private int selectedRow;
     private String errorMessage;
+    // the running integrations, to say who holds the port; the check is cached as binding a socket is not free
+    private Supplier<List<IntegrationInfo>> integrations = List::of;
+    private String checkedPort;
+    private long checkedAt;
+    private String portWarning;
 
     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=" };
@@ -170,6 +176,32 @@ class RunOptionsForm {
         this.errorMessage = error;
     }
 
+    void setIntegrations(Supplier<List<IntegrationInfo>> integrations) {
+        this.integrations = integrations;
+    }
+
+    /**
+     * A warning when the port the app will listen on (the one given, else 8080) is taken already; null when it is free.
+     * Checked again every couple of seconds, or when the port changes.
+     */
+    String portWarning(long now) {
+        String text = portInput != null ? portInput.text().trim() : "";
+        if (text.equals(checkedPort) && now - checkedAt < 2000) {
+            return portWarning;
+        }
+        checkedPort = text;
+        checkedAt = now;
+        int port;
+        try {
+            port = text.isEmpty() ? PortCheck.DEFAULT_PORT : Integer.parseInt(text);
+        } catch (NumberFormatException e) {
+            portWarning = null;
+            return null;
+        }
+        portWarning = PortCheck.warning(port, integrations.get());
+        return portWarning;
+    }
+
     boolean isJaegerExport() {
         return otelAgent && otelExportTarget == 1;
     }
@@ -506,7 +538,8 @@ class RunOptionsForm {
     private void renderOptionsPage(Frame frame, Rect area) {
         // wide enough for the runtime cycler to show all runtimes (Camel Main, Spring Boot, Quarkus, JBang)
         int popupW = Math.min(80, area.width() - 4);
-        int popupH = errorMessage != null ? PAGE1_HEIGHT + 1 : PAGE1_HEIGHT;
+        String warning = portWarning(System.currentTimeMillis());
+        int popupH = PAGE1_HEIGHT + (errorMessage != null ? 1 : 0) + (warning != null ? 1 : 0);
         Rect popup = DialogHelper.centered(area, popupW, popupH);
 
         frame.renderWidget(Clear.INSTANCE, popup);
@@ -606,6 +639,12 @@ class RunOptionsForm {
 
         renderCheckbox(frame, innerX, rowY, innerW, "Java Flight Recorder (JFR)", jfrEnabled, selectedRow == ROW_JFR);
 
+        if (warning != null) {
+            rowY++;
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(TuiIcons.HEALTH_WARN + " " + warning, Theme.warning()))),
+                    new Rect(innerX, rowY, innerW, 1));
+        }
         if (errorMessage != null) {
             rowY++;
             Rect errorArea = new Rect(innerX, rowY, innerW, 1);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
index c97c809c8899..19c1750ca8e6 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelperTest.java
@@ -90,6 +90,23 @@ class DialogHelperTest {
         assertThat(rendered).doesNotContain("Enter");
     }
 
+    @Test
+    void confirmDialogWithADetailAndASecondChoice() {
+        Buffer buffer = Buffer.empty(AREA);
+        Frame frame = Frame.forTesting(buffer);
+
+        Rect popup = DialogHelper.renderConfirm(frame, AREA, "Confirm Quit", "Quit the TUI?",
+                "2 integrations started here keep running", "s", "stop them and quit");
+
+        String rendered = HealthTabRenderTest.bufferToString(buffer);
+        assertThat(rendered).contains("Quit the TUI?");
+        assertThat(rendered).contains("2 integrations started here keep running");
+        assertThat(rendered).contains(" Enter  confirm");
+        assertThat(rendered).contains(" s  stop them and quit");
+        assertThat(rendered).contains(" Esc  cancel");
+        assertThat(popup.height()).isEqualTo(DialogHelper.CONFIRM_HEIGHT + 1);
+    }
+
     @Test
     void inputDialogShowsTitleAndPlaceholder() {
         Buffer buffer = Buffer.empty(AREA);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
index b648b287ea3a..216ce7911d1c 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java
@@ -178,6 +178,29 @@ class PopupManagerTest {
         assertTrue(popupManager.isConfirmVisible(), "a stray key must not dismiss the confirm");
     }
 
+    @Test
+    void confirmWithASecondChoice() {
+        // quitting while integrations started from the TUI run: quit, or stop them and quit
+        List<String> ran = new ArrayList<>();
+        popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", "1 integration started here keeps running",
+                () -> ran.add("quit"), 's', "stop it and quit", () -> ran.add("stop"));
+        assertEquals("s stop it and quit", popupManager.confirmExtraHint());
+
+        popupManager.handleKeyEvent(KeyEvent.ofChar('x'), 0, 2);
+        assertTrue(popupManager.isConfirmVisible());
+        popupManager.handleKeyEvent(KeyEvent.ofChar('s'), 0, 2);
+        assertEquals(List.of("stop"), ran);
+        assertFalse(popupManager.isConfirmVisible());
+
+        // a plain confirm after it has no second choice left over
+        popupManager.showConfirm("Confirm Quit", " Quit? ", () -> ran.add("plain"));
+        assertEquals(null, popupManager.confirmExtraHint());
+        popupManager.handleKeyEvent(KeyEvent.ofChar('s'), 0, 2);
+        assertEquals(List.of("stop"), ran);
+        popupManager.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE), 0, 2);
+        assertEquals(List.of("stop", "plain"), ran);
+    }
+
     @Test
     void killConfirmIgnoresOtherKeysAndCancelsOnEscape() {
         popupManager.showKillConfirm();
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java
new file mode 100644
index 000000000000..8964cfd1df5b
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PortCheckTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The run form warns when the port of the app is taken already, and names the integration that holds it.
+ */
+class PortCheckTest {
+
+    @Test
+    void aTakenPortIsFoundWithItsOwner() throws Exception {
+        try (ServerSocket taken = new ServerSocket()) {
+            taken.bind(new InetSocketAddress(0));
+            int port = taken.getLocalPort();
+            assertThat(PortCheck.inUse(port)).isTrue();
+
+            IntegrationInfo holder = new IntegrationInfo();
+            holder.pid = "42";
+            holder.name = "circuit-breaker";
+            HttpEndpointInfo ep = new HttpEndpointInfo();
+            ep.url = "http://0.0.0.0:" + port + "/hello";
+            holder.httpEndpoints.add(ep);
+
+            assertThat(PortCheck.warning(port, List.of(holder)))
+                    .isEqualTo("Port " + port + " is taken by circuit-breaker; set another if this app serves HTTP");
+            assertThat(PortCheck.warning(port, List.of())).contains("taken by another process");
+        }
+    }
+
+    @Test
+    void aFreePortHasNoWarning() throws Exception {
+        int port;
+        try (ServerSocket probe = new ServerSocket(0)) {
+            port = probe.getLocalPort();
+        }
+        assertThat(PortCheck.inUse(port)).isFalse();
+        assertThat(PortCheck.warning(port, List.of())).isNull();
+        assertThat(PortCheck.inUse(0)).isFalse();
+    }
+}