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 0659ea200596 chore: camel-jbang - camel tui --web serves browsers only 
without a terminal, --defaults, utility routes shown out of the box (#28459)
0659ea200596 is described below

commit 0659ea20059653f19c34998bb08a4ab3a22ff062
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 20:23:51 2026 +0100

    chore: camel-jbang - camel tui --web serves browsers only without a 
terminal, --defaults, utility routes shown out of the box (#28459)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-diagram.adoc        |   2 +-
 .../ROOT/pages/camel-jbang-tui-settings.adoc       |   8 +-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   6 +-
 .../jbang-commands/camel-jbang-tui-monitor.adoc    |   3 +-
 .../ROOT/pages/jbang-commands/camel-jbang-tui.adoc |   3 +-
 .../camel-jbang-plugin-commands-metadata.json      |   2 +-
 .../core/commands/tui/AbstractDiagramTab.java      |  16 +++
 .../jbang/core/commands/tui/ArchitectureView.java  |   3 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  41 +++----
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |   3 +
 .../jbang/core/commands/tui/MonitorStartup.java    |  67 +++++++++++
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |   3 +
 .../dsl/jbang/core/commands/tui/TuiCommand.java    |  15 ++-
 .../dsl/jbang/core/commands/tui/TuiUserConfig.java |  34 ++++++
 .../dsl/jbang/core/commands/tui/TuiWebServer.java  |   7 ++
 .../src/main/resources/tui/help/diagram.md         |   2 +-
 .../core/commands/tui/ArchitectureViewTest.java    |   8 +-
 .../core/commands/tui/DiagramTabRenderTest.java    |  13 ++
 .../core/commands/tui/MonitorStartupTest.java      | 133 +++++++++++++++++++++
 .../jbang/core/commands/tui/SourceSizeTest.java    |   2 +-
 .../jbang/core/commands/tui/TuiUserConfigTest.java |  20 ++++
 21 files changed, 352 insertions(+), 39 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index 4391e0c2a9f8..0a08066a53bb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -184,7 +184,7 @@ Each route has one role:
   every group that uses them.
 * *Utility* -- plumbing with little business meaning: a route reached only 
while handling a failure
   (from a `doCatch`, an `onException` or a dead letter channel), a route that 
only logs, or one the
-  AI marked as utility. Hidden until you press *u*; with *x* on, the routes 
reached only on error are shown
+  AI marked as utility. Shown out of the box, so a project shows all its 
routes; *u* hides them; with *x* on, the routes reached only on error are shown
   in the error handling frame of the topology instead, and in the *Error 
handling* box of the architecture.
 * *Error handling* -- with *x* on, the routes reached only on error, out of 
Utility into a box of their
   own, drawn dashed, with a dashed arrow from each group whose failures go 
there: a route of the group
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 372a7431c1be..835f43a16fd4 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
@@ -155,7 +155,9 @@ Use *↑*/*↓* to move between rows, *Space* (or *←*/*→*) to 
cycle a choice
 field, *Enter* to save, and *Esc* to cancel.
 
 Each row is stored under a `camel.tui.*` key in the Camel CLI configuration 
file, so the same
-setting can also be written there by hand:
+setting can also be written there by hand. To see the TUI as a new user does, 
for a demo or to
+rule out a setting, start it with `camel tui --defaults`: the saved settings 
are not read, and
+what you change lasts for that session only, so your own settings are left as 
they are.
 
 [cols="2,3,6,2",options="header"]
 |===
@@ -300,6 +302,10 @@ This starts a web terminal server on `localhost:8090` 
(configurable with `--web-
 Open `http://localhost:8090` in a browser to get a full xterm.js terminal 
driving the same
 TUI dashboard, with the same tabs, keyboard shortcuts, and F2 actions menu as 
a local session.
 
+Without an interactive terminal -- started from a script, a CI job or a 
background shell -- `camel tui --web`
+serves browsers only: it prints the address and keeps serving until you press 
*Ctrl+C* or stop the
+process. The MCP server drives the screen of the terminal, so `--mcp` needs 
one and is refused in this mode.
+
 Like the MCP server, the web server is bound to `127.0.0.1` only -- it never 
listens on
 external interfaces -- and there is no authentication beyond that. Each 
browser connection
 gets its own independent TUI session (its own process discovery and navigation 
state), the
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 75e0a8f269d5..e3fb93986b63 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -169,7 +169,7 @@ See the 
xref:camel-jbang-tui-source-editor.adoc#_keyboard_shortcuts[keyboard sho
 | `8123`
 
 | `--web`
-| Enable the browser-accessible terminal (WebSocket) server.
+| Enable the browser-accessible terminal (WebSocket) server. Without an 
interactive terminal (a script, a CI job, a background shell) it serves 
browsers only. See xref:camel-jbang-tui-settings.adoc#_web_browser_access[Web 
Browser Access].
 | `false`
 
 | `--web-port`
@@ -188,6 +188,10 @@ See the 
xref:camel-jbang-tui-source-editor.adoc#_keyboard_shortcuts[keyboard sho
 | Color theme of the shell (*F6*) and AI (*F8*) panels for this session, or 
`same` for the main theme. See 
xref:camel-jbang-tui-settings.adoc#_panel_theme[Panel Theme]. Overrides the 
persisted `camel.tui.panelTheme` preference when set.
 |
 
+| `--defaults`
+| Start with the default settings, as a new user sees the TUI: the saved 
`camel.tui.*` settings are not read, and what you change lasts for the session 
only. See xref:camel-jbang-tui-settings.adoc#_settings[Settings].
+| `false`
+
 | `--record`
 | Play a `.tape` file in the TUI and record it to an Asciinema `.cast` file. 
See xref:camel-jbang-tui-recording.adoc#_scripted_recording[Scripted Recording] 
for the tape commands.
 |
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
index 23d4719d6a96..a2e44edb510c 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
@@ -21,6 +21,7 @@ camel tui monitor [options]
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
+| `--defaults` | Start with the default settings, as a new user sees the TUI; 
changes last for the session and are not saved |  | boolean
 | `--mcp` | Enable embedded MCP server for AI agent access to the TUI |  | 
boolean
 | `--mcp-port` | MCP server port | 8123 | int
 | `--panel-theme` | Color theme of the shell (F6) and AI (F8) panels, or 
'same' for the main theme (overrides the Panel Theme setting for this session) 
|  | String
@@ -30,7 +31,7 @@ camel tui monitor [options]
 | `--record-size` | Size of the recorded terminal for --record, as 
<cols>x<rows> | 200x50 | String
 | `--refresh` | Refresh interval in milliseconds | 100 | long
 | `--theme` | Color theme (overrides persisted preference for this session) |  
| String
-| `--web` | Enable browser-accessible terminal (WebSocket) server |  | boolean
+| `--web` | Enable browser-accessible terminal (WebSocket) server; without an 
interactive terminal it serves browsers only |  | boolean
 | `--web-port` | Web terminal server port | 8090 | int
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
index 436fde02a480..3d074bb98469 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
@@ -30,6 +30,7 @@ camel tui [options]
 [cols="2,5,1,2",options="header"]
 |===
 | Option | Description | Default | Type
+| `--defaults` | Start with the default settings, as a new user sees the TUI; 
changes last for the session and are not saved |  | boolean
 | `--mcp` | Enable embedded MCP server for AI agent access to the TUI |  | 
boolean
 | `--mcp-port` | MCP server port | 8123 | int
 | `--panel-theme` | Color theme of the shell (F6) and AI (F8) panels, or 
'same' for the main theme (overrides the Panel Theme setting for this session) 
|  | String
@@ -39,7 +40,7 @@ camel tui [options]
 | `--record-size` | Size of the recorded terminal for --record, as 
<cols>x<rows> | 200x50 | String
 | `--refresh` | Refresh interval in milliseconds | 100 | long
 | `--theme` | Color theme: dark or light (overrides persisted preference for 
this session) |  | String
-| `--web` | Enable browser-accessible terminal (WebSocket) server |  | boolean
+| `--web` | Enable browser-accessible terminal (WebSocket) server; without an 
interactive terminal it serves browsers only |  | boolean
 | `--web-port` | Web terminal server port | 8090 | int
 | `-h,--help` | Display the help and sub-commands |  | boolean
 |===
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
index ccaaca3c9c10..ba0c61b7b15a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
@@ -1,5 +1,5 @@
 {
   "commands": [
-    { "name": "tui", "fullName": "tui", "description": "Camel TUI", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.tui.TuiCommand", 
"plugin": "tui", "options": [ { "names": "--mcp", "description": "Enable 
embedded MCP server for AI agent access to the TUI", "javaType": "boolean", 
"type": "boolean" }, { "names": "--mcp-port", "description": "MCP server port", 
"defaultValue": "8123", "javaType": "int", "type": "integer" }, { "names": 
"--panel-theme", "description": "Color theme o [...]
+    { "name": "tui", "fullName": "tui", "description": "Camel TUI", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.tui.TuiCommand", 
"plugin": "tui", "options": [ { "names": "--defaults", "description": "Start 
with the default settings, as a new user sees the TUI; changes last for the 
session and are not saved", "javaType": "boolean", "type": "boolean" }, { 
"names": "--mcp", "description": "Enable embedded MCP server for AI agent 
access to the TUI", "javaType": "boolean", "type" [...]
   ]
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
index 26dad2c67be3..668d0eb88c54 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
@@ -19,8 +19,10 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
+import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
@@ -114,6 +116,20 @@ abstract class AbstractDiagramTab extends AbstractTab {
         return true;
     }
 
+    /**
+     * The footer of the go to node popup while it is open, in place of the 
diagram's: what is typed goes into the
+     * popup, so the keys of the diagram do not apply.
+     */
+    protected final boolean renderGotoNodeFooter(List<Span> spans) {
+        if (!gotoNodePopup.isVisible()) {
+            return false;
+        }
+        TuiHelper.hint(spans, "type", "filter");
+        TuiHelper.hint(spans, "Enter", "go to");
+        TuiHelper.hintLast(spans, "Esc", "back");
+        return true;
+    }
+
     /** c: the source of the selected node of a route, or of the route 
selected in the topology. */
     protected final boolean handleSourceKey(KeyEvent ke) {
         if (!diagram.isShowDiagram() || !ke.isChar('c')) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index d29e209ea8e3..1cddd092eec4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -102,7 +102,8 @@ final class ArchitectureView {
     private Capabilities picked;
     private Capabilities aiCapabilities;
     private Capabilities factCapabilities;
-    private boolean showUtility;
+    /** Whether the utility routes are shown: on out of the box, so a new 
project shows all its routes (u). */
+    private boolean showUtility = true;
     /** Whether the external systems are drawn as boxes linked to the groups 
that use them (e). */
     private boolean showExternal;
     /** The kinds of an external system box when it is selected: messages come 
in from it, or go out to it. */
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 f498f5d935fb..d81249b92131 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
@@ -66,6 +66,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolConfirms;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.util.json.JsonObject;
 import picocli.CommandLine;
@@ -141,7 +142,8 @@ public class CamelMonitor extends CamelCommand {
     volatile int mcpPort = 8123;
 
     @CommandLine.Option(names = { "--web" },
-                        description = "Enable browser-accessible terminal 
(WebSocket) server")
+                        description = "Enable browser-accessible terminal 
(WebSocket) server; without an interactive"
+                                      + " terminal it serves browsers only")
     boolean web;
 
     @CommandLine.Option(names = { "--web-port" },
@@ -160,6 +162,11 @@ public class CamelMonitor extends CamelCommand {
                         completionCandidates = 
ThemeModeCompletionCandidates.class)
     String panelTheme;
 
+    @CommandLine.Option(names = { "--defaults" },
+                        description = "Start with the default settings, as a 
new user sees the TUI; changes last for the"
+                                      + " session and are not saved")
+    boolean defaults;
+
     // State
     private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
     private TabRegistry tabRegistry;
@@ -390,8 +397,16 @@ public class CamelMonitor extends CamelCommand {
             // the directory is a project to open, not a name filter
             name = "*";
         }
+        if (defaults) {
+            TuiUserConfig.useDefaults();
+        }
         applyThemeOption();
         configureRecording();
+        if (MonitorStartup.browserOnly(web, webBackend != null, record != 
null, EnvironmentHelper.isInteractiveTerminal())) {
+            MonitorStartup.serveBrowserOnly(
+                    new TuiWebServer(webPort, getMain(), classLoader, name, 
refreshInterval, theme), webPort, mcp, printer());
+            return 0;
+        }
         MonitorStartup.requireTerminal(webBackend != null, record != null);
         recordingManager.init(record != null);
 
@@ -426,29 +441,9 @@ public class CamelMonitor extends CamelCommand {
         return 0;
     }
 
-    /**
-     * Validates and applies the --theme and --panel-theme options.
-     */
+    /** Validates and applies the --theme and --panel-theme options. */
     void applyThemeOption() {
-        if (theme != null) {
-            if (!Theme.isValidMode(theme)) {
-                String expected = String.join("' or '", ThemeMode.ids());
-                throw new CommandLine.ParameterException(
-                        new CommandLine(this),
-                        "Invalid value for option '--theme': expected '" + 
expected + "', was '" + theme + "'");
-            }
-            Theme.applyStartupMode(theme);
-        }
-        if (panelTheme != null) {
-            if (!Theme.isValidMode(panelTheme) && 
!Theme.PANEL_THEME_SAME.equalsIgnoreCase(panelTheme.strip())) {
-                String expected = String.join("' or '", ThemeMode.ids());
-                throw new CommandLine.ParameterException(
-                        new CommandLine(this),
-                        "Invalid value for option '--panel-theme': expected '" 
+ Theme.PANEL_THEME_SAME + "' or '" + expected
-                                               + "', was '" + panelTheme + 
"'");
-            }
-            Theme.setPanelTheme(panelTheme);
-        }
+        MonitorStartup.applyThemeOptions(new CommandLine(this), theme, 
panelTheme);
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index d4caf3728253..b643834187ca 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -683,6 +683,9 @@ class DiagramTab extends AbstractDiagramTab {
 
     @Override
     public void renderFooter(List<Span> spans) {
+        if (renderGotoNodeFooter(spans)) {
+            return;
+        }
         if (sourceViewer.isVisible()) {
             sourceViewer.renderFooter(spans);
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
index 965b236f302b..89da80131784 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartup.java
@@ -16,11 +16,14 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.net.BindException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import picocli.CommandLine;
 
 /**
  * What the monitor works out from its options before it starts.
@@ -45,6 +48,31 @@ final class MonitorStartup {
         return null;
     }
 
+    /**
+     * Validates and applies the --theme and --panel-theme options.
+     *
+     * @throws CommandLine.ParameterException for a theme that does not exist
+     */
+    static void applyThemeOptions(CommandLine command, String theme, String 
panelTheme) {
+        if (theme != null) {
+            if (!Theme.isValidMode(theme)) {
+                String expected = String.join("' or '", ThemeMode.ids());
+                throw new CommandLine.ParameterException(
+                        command, "Invalid value for option '--theme': expected 
'" + expected + "', was '" + theme + "'");
+            }
+            Theme.applyStartupMode(theme);
+        }
+        if (panelTheme != null) {
+            if (!Theme.isValidMode(panelTheme) && 
!Theme.PANEL_THEME_SAME.equalsIgnoreCase(panelTheme.strip())) {
+                String expected = String.join("' or '", ThemeMode.ids());
+                throw new CommandLine.ParameterException(
+                        command, "Invalid value for option '--panel-theme': 
expected '" + Theme.PANEL_THEME_SAME + "' or '"
+                                 + expected + "', was '" + panelTheme + "'");
+            }
+            Theme.setPanelTheme(panelTheme);
+        }
+    }
+
     /**
      * Fails the monitor without an interactive terminal (a script, a CI job, 
an AI agent), instead of drawing in none:
      * a browser session (--web) has a terminal of its own, and a recording 
(--record) plays a tape headless.
@@ -56,4 +84,43 @@ final class MonitorStartup {
             throw CliException.usage("camel tui requires an interactive 
terminal");
         }
     }
+
+    /**
+     * Whether the monitor serves browsers only: --web without a terminal to 
draw in (a script, a CI job, a background
+     * shell). Each browser session is a monitor of its own, so the one 
started here needs none.
+     */
+    static boolean browserOnly(boolean web, boolean browserSession, boolean 
recording, boolean interactiveTerminal) {
+        return web && !browserSession && !recording && !interactiveTerminal;
+    }
+
+    /**
+     * Serves the TUI to browsers until the JVM stops (Ctrl+C) or the server 
is stopped. The MCP server drives the
+     * screen of the terminal, which a browser-only monitor does not have, so 
--mcp fails here.
+     *
+     * @throws CliException with exit code 2 for --mcp, or when the port is 
taken
+     */
+    static void serveBrowserOnly(TuiWebServer server, int port, boolean mcp, 
Printer printer) throws Exception {
+        if (mcp) {
+            throw CliException.usage("camel tui --mcp needs an interactive 
terminal: without one, --web serves browsers only");
+        }
+        try {
+            server.start();
+        } catch (BindException e) {
+            throw CliException.usage("Web server failed to start: port " + port
+                                     + " is already in use. Use --web-port to 
choose another one.");
+        }
+        Thread stopper = new Thread(server::stop, "camel-tui-web-stop");
+        Runtime.getRuntime().addShutdownHook(stopper);
+        printer.println("Camel TUI in the browser at http://127.0.0.1:"; + port 
+ "/ (Ctrl+C to stop)");
+        try {
+            server.awaitStop();
+        } finally {
+            server.stop();
+            try {
+                Runtime.getRuntime().removeShutdownHook(stopper);
+            } catch (IllegalStateException e) {
+                // the JVM is already stopping
+            }
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index b3bafca9928d..9cfcc7dd5a90 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -757,6 +757,9 @@ class RoutesTab extends AbstractDiagramTab {
 
     @Override
     public void renderFooter(List<Span> spans) {
+        if (renderGotoNodeFooter(spans)) {
+            return;
+        }
         if (sourceViewer.isVisible()) {
             sourceViewer.renderFooter(spans);
         } else if (diagram.isShowDiagram()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
index 4b856cebee69..474fb0871950 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
@@ -41,7 +41,8 @@ public class TuiCommand extends CamelCommand {
     int mcpPort = 8123;
 
     @CommandLine.Option(names = { "--web" },
-                        description = "Enable browser-accessible terminal 
(WebSocket) server")
+                        description = "Enable browser-accessible terminal 
(WebSocket) server; without an interactive"
+                                      + " terminal it serves browsers only")
     boolean web;
 
     @CommandLine.Option(names = { "--web-port" },
@@ -85,6 +86,11 @@ public class TuiCommand extends CamelCommand {
                         completionCandidates = 
ThemeModeCompletionCandidates.class)
     String panelTheme;
 
+    @CommandLine.Option(names = { "--defaults" },
+                        description = "Start with the default settings, as a 
new user sees the TUI; changes last for the"
+                                      + " session and are not saved")
+    boolean defaults;
+
     public TuiCommand(CamelJBangMain main, ClassLoader classLoader) {
         super(main);
         this.classLoader = classLoader;
@@ -92,8 +98,8 @@ public class TuiCommand extends CamelCommand {
 
     @Override
     public Integer doCall() throws Exception {
-        if (record == null) {
-            // a recording plays a tape headless
+        if (record == null && !web) {
+            // a recording plays a tape headless, and --web without a terminal 
serves browsers only
             requireInteractiveTerminal("camel tui");
         }
         CamelMonitor cmd = new CamelMonitor(getMain(), classLoader);
@@ -153,6 +159,9 @@ public class TuiCommand extends CamelCommand {
             args.add("--panel-theme");
             args.add(panelTheme);
         }
+        if (defaults) {
+            args.add("--defaults");
+        }
         return args;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
index 5017040e0079..cc0efea2efc7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.Printer;
@@ -38,15 +40,37 @@ import org.apache.camel.dsl.jbang.core.common.Printer;
  */
 final class TuiUserConfig {
 
+    /** The settings of a session started with --defaults, kept in memory 
only; null for the files. */
+    private static volatile Map<String, String> session;
+
     private TuiUserConfig() {
     }
 
+    /**
+     * Starts from the default settings and saves nothing (--defaults): a key 
reads as unset until the session sets it,
+     * and stays set for the session only. It holds for the whole JVM, so the 
browser sessions of --web follow it too.
+     */
+    static void useDefaults() {
+        if (session == null) {
+            session = new ConcurrentHashMap<>();
+        }
+    }
+
+    /** Back to the files, after a test of {@link #useDefaults()}. */
+    static void resetForTesting() {
+        session = null;
+    }
+
     /**
      * Reads a single {@code camel.tui.*} key. Returns the local value when 
the key is present in the local file,
      * otherwise the global value, otherwise {@code null}. Propagates a {@link 
RuntimeException} on an I/O failure so
      * the caller can decide whether to fall back or log.
      */
     static String read(String key) {
+        Map<String, String> inMemory = session;
+        if (inMemory != null) {
+            return inMemory.get(key);
+        }
         boolean local = livesInLocal(key);
         String[] holder = { null };
         CommandLineHelper.loadProperties(props -> holder[0] = 
props.getProperty(key), local);
@@ -59,6 +83,16 @@ final class TuiUserConfig {
      * file are preserved. Propagates a {@link RuntimeException} on failure so 
the caller can log.
      */
     static void write(String key, String value) {
+        Map<String, String> inMemory = session;
+        if (inMemory != null) {
+            String trimmed = trimToNull(value);
+            if (trimmed == null) {
+                inMemory.remove(key);
+            } else {
+                inMemory.put(key, trimmed);
+            }
+            return;
+        }
         boolean local = livesInLocal(key);
         try {
             CommandLineHelper.createPropertyFile(local);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
index 531d5fda8244..4510ce49aa98 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java
@@ -161,6 +161,13 @@ class TuiWebServer {
         }
     }
 
+    /** Waits until the server is stopped: a browser-only monitor has nothing 
else to do. */
+    void awaitStop() throws InterruptedException {
+        if (serverChannel != null) {
+            serverChannel.closeFuture().sync();
+        }
+    }
+
     boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
         long deadline = System.nanoTime() + unit.toNanos(timeout);
         boolean bossDone = bossGroup.terminationFuture().await(Math.max(0, 
deadline - System.nanoTime()), TimeUnit.NANOSECONDS);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index 1514eff91eca..6e2c4afb5c6d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -106,7 +106,7 @@ The Architecture level shows the routes grouped by what they
 do for the business, one box per group: route groups from the
 source, the capabilities the AI project overview proposed (`✦`,
 italic), shared services (routes several groups use), Other, and
-Utility (plumbing: error handling, logging; `u` shows it). With
+Utility (plumbing: error handling, logging; `u` hides it). With
 `x` on, the routes reached only on error leave Utility for a dashed
 Error handling box, with dashed arrows from the groups whose
 failures go there. A box lists its route count, entry points,
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
index 867f5a006b06..eba0d126af16 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureViewTest.java
@@ -51,7 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * The capability view of the Diagram tab (CAMEL-25147): groups as boxes with 
their facts, AI-assisted groups marked,
- * Enter opens a group's routes and a route's diagram, Esc goes back up, 
utility hidden until u.
+ * Enter opens a group's routes and a route's diagram, Esc goes back up, 
utility shown until u.
  */
 @Isolated
 @Timeout(60)
@@ -151,14 +151,14 @@ class ArchitectureViewTest {
         assertTrue(screen.contains("Shared services"), screen);
         // the business view: what the capability achieves (the technical view 
shows entry points and systems)
         assertTrue(screen.contains(IntegrationSummaryHints.MARK + "Takes 
orders."), "what it achieves: " + screen);
-        assertFalse(screen.contains("Utility"), "utility hidden by default: " 
+ screen);
+        assertTrue(screen.contains("Utility"), "utility shown by default: " + 
screen);
         // boxes are as wide as their longest line, not wider
         String boxTop = screen.lines().filter(l -> 
l.contains("\u250c")).findFirst().orElseThrow();
         int width = boxTop.indexOf('\u2510', boxTop.indexOf('\u250c')) - 
boxTop.indexOf('\u250c') + 1;
         assertTrue(width <= "platform-http:/orders".length() + 8, "box width " 
+ width + ": " + screen);
 
         view.handleKeyEvent(KeyEvent.ofChar('u'));
-        assertTrue(render(view).contains("Utility"));
+        assertFalse(render(view).contains("Utility"), "u hides it");
     }
 
     @Test
@@ -232,7 +232,7 @@ class ArchitectureViewTest {
         await().atMost(10, TimeUnit.SECONDS).until(() -> 
!view.isLoadingForTesting());
         assertFalse(render(view, 60).contains("Error handling"), "off by 
default");
 
-        // x: dlq, reached only from the doCatch of order, leaves Utility 
(hidden) for a box of its own (CAMEL-25466)
+        // x: dlq, reached only from the doCatch of order, leaves Utility for 
a box of its own (CAMEL-25466)
         errors[0] = true;
         view.refresh();
         String screen = render(view, 60);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
index 66bc9a1a0131..6feca237f415 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTabRenderTest.java
@@ -24,6 +24,7 @@ import dev.tamboui.text.Span;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -94,6 +95,18 @@ class DiagramTabRenderTest {
                 "Footer should be empty (no diagram loaded) or contain 
diagram-related hints");
     }
 
+    @Test
+    void theGoToNodePopupShowsItsOwnKeysInTheFooter() {
+        DiagramTab tab = new DiagramTab(ctx);
+        tab.gotoNodePopup.open(List.of(), -1);
+        List<Span> footerSpans = new ArrayList<>();
+        tab.renderFooter(footerSpans);
+
+        String footer = footerSpans.stream().map(Span::content).reduce("", 
String::concat);
+        assertEquals(" type  filter   Enter  go to   Esc  back", footer,
+                "what is typed goes into the popup, so the keys of the diagram 
are not listed");
+    }
+
     @Test
     void renderShowsLoadingWhenNoRoutes() {
         DiagramTab tab = new DiagramTab(ctx);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartupTest.java
new file mode 100644
index 000000000000..00aa4f3afb06
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorStartupTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.Socket;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+class MonitorStartupTest {
+
+    @Test
+    void webWithoutATerminalServesBrowsersOnly() {
+        assertThat(MonitorStartup.browserOnly(true, false, false, 
false)).isTrue();
+        assertThat(MonitorStartup.browserOnly(true, false, false, true)).as("a 
terminal to draw in").isFalse();
+        assertThat(MonitorStartup.browserOnly(false, false, false, 
false)).as("no --web").isFalse();
+        assertThat(MonitorStartup.browserOnly(true, true, false, false)).as("a 
browser session itself").isFalse();
+        assertThat(MonitorStartup.browserOnly(true, false, true, false)).as("a 
recording").isFalse();
+    }
+
+    @Test
+    void camelTuiForwardsDefaults() {
+        TuiCommand command = new TuiCommand(new CamelJBangMain(), 
getClass().getClassLoader());
+        new CommandLine(command).parseArgs("--web", "--defaults");
+        assertThat(command.buildArgs()).containsExactly("--web", "--defaults");
+
+        CamelMonitor monitor = new CamelMonitor(new CamelJBangMain(), 
getClass().getClassLoader());
+        new 
CommandLine(monitor).parseArgs(command.buildArgs().toArray(String[]::new));
+        assertThat(monitor.web).isTrue();
+        assertThat(monitor.defaults).isTrue();
+    }
+
+    @Test
+    void browserOnlyRefusesMcp() {
+        TuiWebServer server = newServer(0);
+
+        assertThatThrownBy(() -> MonitorStartup.serveBrowserOnly(server, 0, 
true, new ListPrinter()))
+                .isInstanceOf(CliException.class)
+                .hasMessageContaining("--mcp needs an interactive terminal");
+    }
+
+    @Test
+    void browserOnlyServesUntilTheServerStops() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            int port = reserved.getPort();
+            TuiWebServer server = newServer(port);
+            ListPrinter printer = new ListPrinter();
+            AtomicReference<Throwable> failure = new AtomicReference<>();
+            Thread serving = new Thread(() -> {
+                try {
+                    MonitorStartup.serveBrowserOnly(server, port, false, 
printer);
+                } catch (Throwable e) {
+                    failure.set(e);
+                }
+            });
+            serving.start();
+
+            await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+                try (Socket socket = new Socket()) {
+                    socket.connect(new InetSocketAddress("127.0.0.1", port), 
500);
+                }
+            });
+            assertThat(printer.lines).containsExactly(
+                    "Camel TUI in the browser at http://127.0.0.1:"; + port + 
"/ (Ctrl+C to stop)");
+            assertThat(serving.isAlive()).as("waits while the server 
runs").isTrue();
+
+            server.stop();
+            serving.join(TimeUnit.SECONDS.toMillis(10));
+            assertThat(serving.isAlive()).isFalse();
+            assertThat(failure.get()).isNull();
+        }
+    }
+
+    private static TuiWebServer newServer(int port) {
+        return new TuiWebServer(
+                port, new CamelJBangMain(), 
Thread.currentThread().getContextClassLoader(), null, 200, "dark",
+                terminal -> {
+                });
+    }
+
+    private static final class ListPrinter implements Printer {
+
+        private final List<String> lines = new CopyOnWriteArrayList<>();
+
+        @Override
+        public void println() {
+            lines.add("");
+        }
+
+        @Override
+        public void println(String line) {
+            lines.add(line);
+        }
+
+        @Override
+        public void print(String output) {
+            lines.add(output);
+        }
+
+        @Override
+        public void printf(String format, Object... args) {
+            lines.add(String.format(format, args));
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index 7fe4ceb9278b..cd486eb9b083 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -59,7 +59,7 @@ class SourceSizeTest {
             AiPanel.java 3180
             AiPanel.runAgentLoop 159
             AiUsageStats.render 166
-            CamelMonitor.java 2761
+            CamelMonitor.java 2756
             DiagramSupport.java 2360
             DiagramSupport.loadHighlightedNativeDiagramInBackground 200
             EndpointsTab.renderContent 170
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
index ab88f752bd6c..bc136d567b3e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
@@ -45,11 +45,31 @@ class TuiUserConfigTest {
 
     @AfterEach
     void tearDown() {
+        TuiUserConfig.resetForTesting();
         if (originalHome != null) {
             CommandLineHelper.useHomeDir(originalHome);
         }
     }
 
+    @Test
+    void defaultsReadNothingAndSaveNothing(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(globalFile, "camel.tui.theme=light\n");
+
+        TuiUserConfig.useDefaults();
+        assertNull(TuiUserConfig.read("camel.tui.theme"), "the saved theme is 
not read");
+
+        TuiUserConfig.write("camel.tui.theme", "dracula");
+        assertEquals("dracula", TuiUserConfig.read("camel.tui.theme"), "a 
change lasts for the session");
+        TuiUserConfig.write("camel.tui.theme", "");
+        assertNull(TuiUserConfig.read("camel.tui.theme"));
+        assertEquals("camel.tui.theme=light\n", Files.readString(globalFile), 
"the file is left as it was");
+
+        TuiUserConfig.resetForTesting();
+        assertEquals("light", TuiUserConfig.read("camel.tui.theme"));
+    }
+
     private void useHome(Path dir) {
         originalHome = CommandLineHelper.getHomeDir().toString();
         CommandLineHelper.useHomeDir(dir.toString());

Reply via email to