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 357ca829ba44 chore: camel-jbang - first row selected on Beans,
Configuration, Heap Histogram and Threads, the Timeline cursor hides the axis
label it is on, a JFR hint, and MCP waiting for a client is no error (#29477)
357ca829ba44 is described below
commit 357ca829ba44d1439aab360c284d2b5fab79a86b
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 14:49:30 2026 +0100
chore: camel-jbang - first row selected on Beans, Configuration, Heap
Histogram and Threads, the Timeline cursor hides the axis label it is on, a JFR
hint, and MCP waiting for a client is no error (#29477)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-ai-agents.adoc | 2 +-
.../dsl/jbang/core/commands/tui/BeansTab.java | 6 +++++
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 5 ++--
.../jbang/core/commands/tui/CircuitBreakerTab.java | 3 ++-
.../jbang/core/commands/tui/ConfigurationTab.java | 6 +++++
.../jbang/core/commands/tui/HeapHistogramTab.java | 6 +++++
.../camel/dsl/jbang/core/commands/tui/JfrTab.java | 13 +++++++++-
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 6 +++++
.../dsl/jbang/core/commands/tui/TimelineTab.java | 23 +++++++++++-------
.../commands/tui/ConfigurationTabRenderTest.java | 16 +++++++++++++
.../jbang/core/commands/tui/JfrTabRenderTest.java | 28 ++++++++++++++++++++++
.../core/commands/tui/TimelineTabRenderTest.java | 18 ++++++++++++++
12 files changed, 118 insertions(+), 14 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
index 44fd28832b43..bd067e88f971 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
@@ -49,7 +49,7 @@ The server is bound to `127.0.0.1` only -- it never listens
on external interfac
When MCP is active, the TUI footer shows the connection status: the port and
the name of the connected client,
with ● while it calls tools and ○ while it is idle (it stays connected while
idle, as an MCP client only sends a
-request when it uses a tool), or ✗ when no client has connected yet or the
client closed its session.
+request when it uses a tool), or _no client_ when no client has connected yet
or the client closed its session.
Use *F2* -> _MCP Info_ to see server details and _MCP Log_ to view the tool
call history.
== Status document resources
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
index 25ca8a0d6c78..426f77ec21de 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
@@ -65,6 +65,12 @@ class BeansTab extends AbstractTableTab {
return sortedBeans().size();
}
+ @Override
+ protected boolean selectsFirstRow() {
+ // the detail panel shows the selected row
+ return true;
+ }
+
@Override
public void onTabSelected() {
String pid = ctx.selectedPid;
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 d53884bacde7..204eb57dac71 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
@@ -2348,9 +2348,10 @@ public class CamelMonitor extends CamelCommand {
labelStyle = Theme.success();
suffixStyle = active ? Theme.success() : Theme.muted();
} else {
- suffix = " " + TuiIcons.CROSS;
+ // the server runs and waits for an agent: not an error
+ suffix = " no client";
labelStyle = Theme.muted();
- suffixStyle = Theme.error();
+ suffixStyle = Theme.muted();
}
rightSpans.add(Span.styled(mcpLabel, labelStyle));
rightSpans.add(Span.styled(suffix, suffixStyle));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
index cf5f1c612a5f..544d980ae293 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
@@ -90,7 +90,8 @@ class CircuitBreakerTab extends AbstractTableTab {
String fallback = cb.fallbackCalls > 0 ?
String.valueOf(cb.fallbackCalls) : "";
String timeout = cb.timedOutCalls > 0 ?
String.valueOf(cb.timedOutCalls) : "";
String rate = cb.failureRate >= 0 ? String.format("%.0f%%",
cb.failureRate) : "";
- String inflight = String.valueOf(cb.inflight);
+ // blank when zero, as the other counts, so the ones that are not
stand out
+ String inflight = cb.inflight > 0 ? String.valueOf(cb.inflight) :
"";
String sinceLast = formatSinceLast(cb.sinceLastStarted,
cb.sinceLastSuccess, cb.sinceLastFail);
rows.add(Row.from(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index e098ea368f89..2cbe60b01b1e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -81,6 +81,12 @@ class ConfigurationTab extends AbstractTableTab {
return info != null ? info.configProperties.size() : 0;
}
+ @Override
+ protected boolean selectsFirstRow() {
+ // the detail panel shows the selected row
+ return true;
+ }
+
@Override
protected boolean handleTabKeyEvent(KeyEvent ke) {
if (ke.isKey(KeyCode.TAB)) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
index 036fe978593b..629c81905ec2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -67,6 +67,12 @@ class HeapHistogramTab extends AbstractTableTab {
return sortedEntries().size();
}
+ @Override
+ protected boolean selectsFirstRow() {
+ // the detail panel shows the selected row
+ return true;
+ }
+
@Override
public void onTabSelected() {
String pid = ctx.selectedPid;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
index 11a221df6dfb..d03e1ffb0540 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
@@ -330,9 +330,20 @@ class JfrTab extends AbstractTab {
String msg = loading.get()
? " Loading..."
: " Press F5 to refresh JFR snapshot data";
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.styled(msg, LABEL)));
+ if (statusLoaded && recordings.isEmpty() && !loading.get()) {
+ // a snapshot needs a recording: say how to start one
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line
+ .from(Span.styled(" No JFR recording is active. Run
the integration with Java Flight Recorder (JFR)"
+ + " checked in its run options (F2 →
Open Project...),",
+ LABEL)));
+ lines.add(Line.from(Span.styled(" or start one with: jcmd " +
ctx.selectedPid + " JFR.start", LABEL)));
+ }
frame.renderWidget(
Paragraph.builder()
- .text(Text.from(Line.from(Span.styled(msg,
LABEL))))
+ .text(Text.from(lines))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
.borderStyle(ctx.paneBorder(true))
.title(" " + activeView.label + "
").build())
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
index 376e391aaa99..dc0b3b88c9a6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
@@ -71,6 +71,12 @@ class ThreadsTab extends AbstractTableTab {
return sortedThreads().size();
}
+ @Override
+ protected boolean selectsFirstRow() {
+ // the detail panel shows the selected row
+ return true;
+ }
+
@Override
public void onTabSelected() {
String pid = ctx.selectedPid;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
index ac381233f9ac..641d1231a954 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
@@ -654,15 +654,14 @@ class TimelineTab extends AbstractTableTab {
Arrays.fill(axis, ' ');
String start = "-" + axisTime(seconds());
String middle = "-" + axisTime(seconds() / 2);
- String end = "now";
- put(axis, 0, start);
- put(axis, columns / 2 - middle.length() / 2, middle);
- put(axis, columns - end.length(), pausedAt >= 0 ? "" : end);
- if (pausedAt >= 0) {
- long ago = Math.max(0, nowSecond() - pausedAt);
- String at = ago > 0 ? "-" + ChaosTab.shortTime(ago * 1000) : end;
- put(axis, columns - at.length(), at);
- }
+ long pausedAgo = pausedAt >= 0 ? Math.max(0, nowSecond() - pausedAt) :
0;
+ String end = pausedAgo > 0 ? "-" + ChaosTab.shortTime(pausedAgo *
1000) : "now";
+ // a label the cursor is on is left out: the time of the cursor is at
the right of the axis
+ int from = cursor >= 0 ? cursor * width : -1;
+ int to = cursor >= 0 ? from + width : -1;
+ label(axis, 0, start, from, to);
+ label(axis, columns / 2 - middle.length() / 2, middle, from, to);
+ label(axis, columns - end.length(), end, from, to);
if (cursor >= 0) {
// the cursor under its column, the time of it at the right of the
axis
for (int i = 0; i < width; i++) {
@@ -690,6 +689,12 @@ class TimelineTab extends AbstractTableTab {
return seconds <= 90 ? seconds + "s" : ChaosTab.shortTime(seconds *
1000L);
}
+ private static void label(char[] axis, int at, String s, int from, int to)
{
+ if (at >= to || at + s.length() <= from) {
+ put(axis, at, s);
+ }
+ }
+
private static void put(char[] axis, int at, String s) {
for (int i = 0; i < s.length() && at + i < axis.length; i++) {
if (at + i >= 0) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTabRenderTest.java
index b10edea7d037..19a6e9875089 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTabRenderTest.java
@@ -65,6 +65,8 @@ class ConfigurationTabRenderTest {
@Test
void renderPropertyKeyInCyan() {
addProperty("camel.main.name", "my-app", "application.properties");
+ // a second row: the first is selected, and the selection highlight
hides its colors
+ addProperty("camel.main.shutdownTimeout", "30",
"application.properties");
ConfigurationTab tab = new ConfigurationTab(ctx);
@@ -77,6 +79,18 @@ class ConfigurationTabRenderTest {
"Property key should be rendered in CYAN");
}
+ @Test
+ void theFirstPropertyIsSelectedSoItsDetailShows() {
+ addProperty("camel.main.name", "my-app", "application.properties");
+ addProperty("camel.main.shutdownTimeout", "30",
"application.properties");
+
+ ConfigurationTab tab = new ConfigurationTab(ctx);
+ String rendered = TuiTestHelper.renderToString(tab, 120, 30);
+
+ assertEquals(0, tab.tableState.selected());
+ assertTrue(!rendered.contains("Select a property"), rendered);
+ }
+
@Test
void renderEmptyShowsPlaceholder() {
ConfigurationTab tab = new ConfigurationTab(ctx);
@@ -125,6 +139,8 @@ class ConfigurationTabRenderTest {
@Test
void renderSecretValueInDarkGray() {
addProperty("camel.vault.password", "xxxxxx",
"application.properties");
+ // a row before it: the first is selected, and the selection highlight
hides its colors
+ addProperty("camel.main.name", "my-app", "application.properties");
ConfigurationTab tab = new ConfigurationTab(ctx);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
index 96bb71294a66..c6a18163c332 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
@@ -85,6 +85,34 @@ class JfrTabRenderTest {
.contains("JFR runtime instrumentation is not available"));
}
+ @Test
+ void withoutARecordingTheTabSaysHowToStartOne() {
+ JsonObject noRecording = new JsonObject();
+ noRecording.put("runtimeEvents", false);
+ noRecording.put("recordings", new JsonArray());
+ TestMonitorContext statusCtx = new TestMonitorContext(dataWith(info),
noRecording);
+ statusCtx.selectedPid = "1234";
+ JfrTab tab = new JfrTab(statusCtx, Runnable::run);
+
+ tab.onTabSelected();
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(TuiTestHelper.renderToString(tab, 160, 20))
+ .contains("No JFR recording is active").contains("Java Flight
Recorder (JFR)")
+ .contains("jcmd 1234 JFR.start"));
+ }
+
+ @Test
+ void withARecordingThereIsNoHint() {
+ TestMonitorContext statusCtx = new TestMonitorContext(dataWith(info),
statusResponse());
+ statusCtx.selectedPid = "1234";
+ JfrTab tab = new JfrTab(statusCtx, Runnable::run);
+
+ tab.onTabSelected();
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(TuiTestHelper.renderToString(tab, 160, 20))
+ .contains("Runtime Events: Enabled").doesNotContain("No JFR
recording is active"));
+ }
+
@Test
void renderFooterHints() {
JfrTab tab = new JfrTab(ctx);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
index 7a3a3cbf862c..c5b277032565 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java
@@ -285,6 +285,24 @@ class TimelineTabRenderTest {
assertThat(recorded.stream().filter(v -> ((Number) v).longValue() ==
2).count()).isEqualTo(10);
}
+ @Test
+ void theCursorHidesTheLabelOfTheAxisItIsOn() {
+ TimelineTab tab = new TimelineTab(ctx, now::get);
+ TuiTestHelper.renderToString(tab, 160, 40);
+ // paused, the cursor is on the last second, where the axis says now
+ tab.handleKeyEvent(KeyEvent.ofChar(' '));
+ String axis = TuiTestHelper.renderToString(tab, 160,
40).lines().filter(l -> l.contains("-60s")).findFirst()
+ .orElseThrow();
+ // no piece of a label next to the cursor; its time is at the right
+ assertThat(axis).doesNotContainPattern("[^ ▲]▲").contains("▲ now");
+ // the cursor on the start of the axis
+ for (int i = 0; i < 59; i++) {
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT));
+ }
+ axis = TuiTestHelper.renderToString(tab, 160, 40).lines().filter(l ->
l.contains("-30s")).findFirst().orElseThrow();
+ assertThat(axis).doesNotContain("-60s").doesNotContainPattern("▲[^
▲]");
+ }
+
@Test
void enterShowsTheExchangesOfTheSecondAtTheCursorAndOneInDetail() {
TimelineTab tab = new TimelineTab(ctx, now::get);