Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,25 @@ The MCP server exposes tools organized by purpose:

See xref:camel-jbang-mcp.adoc[Camel MCP Server] for more about MCP and AI integration with Camel.

== Web Browser Access

The TUI can also be reached from a web browser on the same host, using the same dashboard you'd see in a
local terminal -- useful when you prefer a browser session to a terminal window.

[source,bash]
----
camel tui --web
----

This starts a web terminal server on `localhost:8090` (configurable with `--web-port`).
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.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low — docs] States browser sessions have "the same … keyboard shortcuts" as local terminal, but web sessions hide the q/quit footer hint (OverviewTab) and swallow q/Ctrl+C (CamelMonitor L795–798). Consider noting that quit behaves differently in browser sessions.

same as if you'd run `camel tui` again in a new terminal.

== Recording Demos

The TUI can record terminal sessions for demos and documentation.
Expand Down Expand Up @@ -809,6 +828,14 @@ vhs demo.tape # .tape -> .gif
| Port for the embedded MCP server.
| `8123`

| `--web`
| Enable the browser-accessible terminal (WebSocket) server.
| `false`

| `--web-port`
| Port for the web terminal server.
| `8090`

| `--refresh`
| Screen refresh interval in milliseconds.
| `100`
Expand Down
17 changes: 15 additions & 2 deletions docs/user-manual/modules/ROOT/pages/security-model.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,17 @@ be closed as `not a vulnerability`.
"MBean operation X executes code or sends to endpoint Y when invoked
from a JMX or Jolokia connection" describes the documented contract,
not a framework vulnerability.
* *The Camel TUI's `--mcp` and `--web` servers.* Camel TUI

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info — looks good] Clear framing of --web alongside --mcp as opt-in loopback management surfaces. Matches Camel's documented trust model.

(`dsl/camel-jbang/camel-jbang-plugin-tui`) can optionally expose an MCP
server (`--mcp`, for AI-agent access) and a browser-accessible terminal
(`--web`, over WebSocket). Both are management surfaces under the same
framing as above: opt-in (off unless the flag is passed), bound to
`127.0.0.1` only, and - like JMX and Jolokia without an auth layer
configured - carry no authentication of their own beyond that loopback
bind. Full interactive control of the TUI (including anything it can
shell out to, such as `docker`/`podman`/`camel run`) reachable from
`127.0.0.1` is the documented contract for both flags, not a framework
vulnerability.
* *Vulnerabilities in third-party transitive dependencies.* Camel fixes
CVEs in Camel code, not in third-party JARs; the report belongs
upstream. Camel may upgrade the dependency to pick up an upstream fix,
Expand Down Expand Up @@ -1084,8 +1095,10 @@ YAML::
upstream broker is not inside the trust boundary. If the option is
unavoidable, install an `ObjectInputFilter`.
* *Do not expose management surfaces.* `camel-management`, the developer
console, `camel-jolokia` and JMX should listen on a loopback interface, a
sidecar, or a separate network only.
console, `camel-jolokia`, JMX, and Camel TUI's `--mcp` / `--web` servers
should listen on a loopback interface, a sidecar, or a separate network
only. The TUI servers already default to `127.0.0.1`; do not front them
with a reverse proxy that makes them reachable from a public network.
* *Keep components patched.* Pin Camel to a supported version, subscribe to
the announce list, and respond to advisories at
link:/security/[].
Expand Down
59 changes: 59 additions & 0 deletions dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,34 @@
<artifactId>tamboui-markdown</artifactId>
<version>${tamboui-version}</version>
</dependency>
<dependency>
<groupId>dev.tamboui</groupId>
<artifactId>tamboui-aesh-backend</artifactId>
<version>${tamboui-version}</version>
</dependency>
<dependency>
<groupId>org.aesh</groupId>
<artifactId>terminal-http</artifactId>
<version>${aesh-terminal-version}</version>
</dependency>
<!-- Netty is pulled in transitively by terminal-http, but TuiWebServer uses these
packages directly, so declare them explicitly to keep the contract clear. -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-junit6</artifactId>
Expand All @@ -105,4 +133,35 @@
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.rat</groupId>
<artifactId>apache-rat-plugin</artifactId>
<configuration>
<excludes combine.children="append">
<!-- vendored MIT-licensed xterm.js assets; see src/main/resources/tui/web/vendor/LICENSE-xterm.txt -->
<exclude>**/tui/web/vendor/*.js</exclude>
<exclude>**/tui/web/vendor/*.css</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<configuration>
<licenseSets>
<licenseSet>
<excludes combine.children="append">
<!-- vendored MIT-licensed xterm.js assets; see src/main/resources/tui/web/vendor/LICENSE-xterm.txt -->
<exclude>**/tui/web/vendor/*.js</exclude>
<exclude>**/tui/web/vendor/*.css</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.net.BindException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -40,6 +41,7 @@
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Color;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Backend;
import dev.tamboui.terminal.Frame;
import dev.tamboui.text.CharWidth;
import dev.tamboui.text.Line;
Expand Down Expand Up @@ -108,6 +110,15 @@ public class CamelMonitor extends CamelCommand {
defaultValue = "8123")
int mcpPort = 8123;

@CommandLine.Option(names = { "--web" },
description = "Enable browser-accessible terminal (WebSocket) server")
boolean web;

@CommandLine.Option(names = { "--web-port" },
description = "Web terminal server port (default: ${DEFAULT-VALUE})",
defaultValue = "8090")
int webPort = 8090;

@CommandLine.Option(names = { "--theme" },
description = "Color theme (overrides persisted preference for this session)",
completionCandidates = ThemeModeCompletionCandidates.class)
Expand All @@ -127,6 +138,7 @@ public class CamelMonitor extends CamelCommand {
private String lastWaveNotification;
private boolean mcpInjectedKey;
private TuiMcpServer mcpServer;
private TuiWebServer webServer;
private McpFacade mcpFacade;
private final Queue<McpFacade.PendingKey> pendingKeys = new ConcurrentLinkedQueue<>();
private final CaptionOverlay captionOverlay = new CaptionOverlay();
Expand All @@ -141,6 +153,9 @@ public class CamelMonitor extends CamelCommand {

private ActionsPopup actionsPopup;
private TuiRunner runner;
// Set by TuiWebServer for browser sessions; local terminal sessions leave this null
// and let TuiBackendHelper auto-detect the active terminal instead.
Backend webBackend;

private MonitorContext ctx;

Expand Down Expand Up @@ -514,7 +529,7 @@ public void resetIntegrationTabState() {
actionsPopup.setMcpEnabled(true, mcpPort, mcpServer::getConnectedClient,
mcpServer::getActivityLog, mcpServer::getToolCallCount);
mcpJsonFile = writeMcpJson(mcpPort);
} catch (java.net.BindException e) {
} catch (BindException e) {
System.err.println("MCP server failed to start: port " + mcpPort + " is already in use.");
System.err.println("Use --mcp-port to specify a different port, e.g.: camel tui --mcp --mcp-port 8124");
mcpServer = null;
Expand All @@ -523,7 +538,21 @@ public void resetIntegrationTabState() {
}
aiPanel.setMcpInfo(mcp, mcpPort);

try (var tui = TuiBackendHelper.createTuiRunner()) {
if (web) {
webServer = new TuiWebServer(webPort, getMain(), classLoader, name, refreshInterval, theme);
try {
webServer.start();
} catch (BindException e) {
System.err.println("Web server failed to start: port " + webPort + " is already in use.");
System.err.println("Use --web-port to specify a different port, e.g.: camel tui --web --web-port 8091");
webServer = null;
web = false;
}
}

try (var tui = webBackend != null
? TuiBackendHelper.createTuiRunner(webBackend)
: TuiBackendHelper.createTuiRunner()) {
this.runner = tui;
aiPanel.setExitCallbackForTestingOrRuntime(tui::quit);
ctx.runner = tui;
Expand All @@ -539,9 +568,14 @@ public void resetIntegrationTabState() {
applyLogPin();
applyRatePer();
applyConfirmActions();
// Intercept Ctrl+C: quit the TUI cleanly instead of letting
// the JVM tear down the classloader while we're still running
Signal.handle(new Signal("INT"), sig -> tui.quit());
if (webBackend == null) {
// Intercept Ctrl+C: quit the TUI cleanly instead of letting
// the JVM tear down the classloader while we're still running.
// Signal.handle is process-wide and would clobber concurrent sessions
// (e.g. browser connections via --web), so only the local terminal
// session registers it.
Signal.handle(new Signal("INT"), sig -> tui.quit());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info — looks good] Correctly scopes Signal.handle(INT) to the local terminal session only. Process-wide signal handlers would break concurrent browser sessions.

}
tui.run(
this::handleEvent,
this::render);
Expand All @@ -552,6 +586,9 @@ public void resetIntegrationTabState() {
if (mcpServer != null) {
mcpServer.stop();
}
if (webServer != null) {
webServer.stop();
}
deleteMcpJson(mcpJsonFile);
this.runner = null;
}
Expand Down Expand Up @@ -754,6 +791,8 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) {
boolean textEditing = probeEditing || sourceSearchActive || logSearchActive || spanFilterActive
|| beanFilterActive || classpathFilterActive || mavenDepsFilterActive || sqlInputActive
|| catalogFilterActive || filesBrowserTextActive;
// Each session (the local terminal, or a browser tab connected via --web) owns an
// independent CamelMonitor/TuiRunner, so quitting here only ends this session.
if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
if (!ke.isCtrlC() && ctx.confirmActions) {
popupManager.showConfirm("Confirm Quit", " Quit the TUI? ", () -> runner.quit());
Expand Down Expand Up @@ -1394,6 +1433,11 @@ private void renderHeader(Frame frame, Rect area) {
titleSpans.add(Span.raw(" "));
titleSpans.add(Span.styled(activeInfra + " infra(s)", Theme.notice()));
}
if (web && webBackend == null) {
titleSpans.add(Span.raw(" "));
titleSpans.add(Span.styled("web :" + webPort,
Theme.notice().underlined().hyperlink("http://127.0.0.1:" + webPort + "/")));
}
if (ctx.selectedPid != null) {
titleSpans.add(Span.raw(" "));
InfraInfo selInfra = findSelectedInfra();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.apache.camel.dsl.jbang.core.commands.tui;

import dev.tamboui.backend.jline3.JLineBackend;
import dev.tamboui.terminal.Backend;
import dev.tamboui.tui.TuiConfig;
import dev.tamboui.tui.TuiRunner;
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
Expand All @@ -29,10 +30,15 @@ private TuiBackendHelper() {

static TuiRunner createTuiRunner() throws Exception {
Terminal activeTerminal = EnvironmentHelper.getActiveTerminal();
if (activeTerminal != null) {
JLineBackend backend = new JLineBackend(activeTerminal);
return TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build());
}
return TuiRunner.create(TuiConfig.builder().mouseCapture(true).build());
// Build the JLine backend explicitly rather than leaving backend selection to
// TamboUI's ServiceLoader-based auto-discovery: with tamboui-aesh-backend also on the
// classpath (for --web), auto-discovery can pick AeshBackend for the local session too,
// which drives a native PosixSysTerminal that doesn't shut down cleanly here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info — looks good] Explicit JLine backend avoids ServiceLoader picking AeshBackend for the local session when tamboui-aesh-backend is on the classpath. Good fix for clean shutdown.

JLineBackend backend = activeTerminal != null ? new JLineBackend(activeTerminal) : new JLineBackend();
return TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build());
}

static TuiRunner createTuiRunner(Backend backend) throws Exception {
return TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public class TuiCommand extends CamelCommand {
defaultValue = "8123")
int mcpPort = 8123;

@CommandLine.Option(names = { "--web" },
description = "Enable browser-accessible terminal (WebSocket) server")
boolean web;

@CommandLine.Option(names = { "--web-port" },
description = "Web terminal server port (default: ${DEFAULT-VALUE})",
defaultValue = "8090")
int webPort = 8090;

@CommandLine.Option(names = { "--refresh" },
description = "Refresh interval in milliseconds (default: ${DEFAULT-VALUE})",
defaultValue = "100")
Expand Down Expand Up @@ -73,6 +82,13 @@ public Integer doCall() throws Exception {
args.add("--mcp-port");
args.add(String.valueOf(mcpPort));
}
if (web) {
args.add("--web");
}
if (webPort != 8090) {
args.add("--web-port");
args.add(String.valueOf(webPort));
}
if (refreshInterval != 100) {
args.add("--refresh");
args.add(String.valueOf(refreshInterval));
Expand Down
Loading