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 60b9e750cfd9 CAMEL-24259: camel-tui - Add Internal Tasks tab
60b9e750cfd9 is described below

commit 60b9e750cfd914fe8695c2dbf69a9ea6e441ee7a
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 27 14:35:48 2026 +0200

    CAMEL-24259: camel-tui - Add Internal Tasks tab
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../core/commands/process/ListInternalTask.java    |   6 +-
 .../jbang/core/commands/tui/IntegrationInfo.java   |   1 +
 .../jbang/core/commands/tui/InternalTaskInfo.java  |  29 +++
 .../jbang/core/commands/tui/InternalTasksTab.java  | 241 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  22 ++
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |   6 +
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   1 +
 .../jbang/core/commands/tui/TabRegistryTest.java   |   6 +-
 8 files changed, 306 insertions(+), 6 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
index 17c89c00c6cc..85eb4444b568 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
@@ -36,12 +36,12 @@ import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
 
-@Command(name = "internal-tasks",
+@Command(name = "internal-task",
          description = "List internal tasks of Camel integrations", 
sortOptions = false, showDefaultValues = true,
          footer = {
                  "%nExamples:",
-                 "  camel get internal-tasks",
-                 "  camel get internal-tasks --watch" })
+                 "  camel get internal-task",
+                 "  camel get internal-task --watch" })
 public class ListInternalTask extends ProcessWatchCommand {
 
     @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 2cb345b043d7..622952024ac7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -101,6 +101,7 @@ class IntegrationInfo {
     final List<MicrometerMeterInfo> meters = new ArrayList<>();
     final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
     final List<ConfigurationTab.ConfigProperty> configProperties = new 
ArrayList<>();
+    final List<InternalTaskInfo> internalTasks = new ArrayList<>();
     final List<DataSourceInfo> dataSources = new ArrayList<>();
     final List<SqlTraceInfo> sqlTraceStatements = new ArrayList<>();
     long sqlTraceTotal;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTaskInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTaskInfo.java
new file mode 100644
index 000000000000..23ee67a0615d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTaskInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+class InternalTaskInfo {
+    String name;
+    String status;
+    long attempts;
+    long delay;
+    long elapsed;
+    long firstTime;
+    long lastTime;
+    long nextTime;
+    String error;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTasksTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTasksTab.java
new file mode 100644
index 000000000000..61c7b476c8f8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTasksTab.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class InternalTasksTab extends AbstractTableTab {
+
+    InternalTasksTab(MonitorContext ctx) {
+        super(ctx, "name", "status", "attempts", "elapsed");
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.internalTasks.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        List<InternalTaskInfo> sorted = new ArrayList<>(info.internalTasks);
+        sorted.sort(this::sortTask);
+
+        List<Row> rows = new ArrayList<>();
+        for (InternalTaskInfo ti : sorted) {
+            Style statusStyle = statusStyle(ti.status);
+            String elapsed = TimeUtils.printAge(ti.elapsed);
+            String first = ti.firstTime > 0 ? 
TimeUtils.printSince(ti.firstTime) : "";
+            String last = ti.lastTime > 0 ? TimeUtils.printSince(ti.lastTime, 
true) : "";
+            String next = formatNext(ti.nextTime);
+            String error = ti.error != null ? ti.error : "";
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(" " + (ti.name != null ? ti.name : 
""), Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(Span.styled(ti.status != null ? ti.status : "", 
statusStyle)),
+                    rightCell(String.valueOf(ti.attempts), 8),
+                    rightCell(String.valueOf(ti.delay), 8),
+                    Cell.from(elapsed),
+                    Cell.from(first),
+                    Cell.from(last),
+                    Cell.from(next),
+                    Cell.from(Span.styled(error, error.isEmpty() ? Style.EMPTY 
: Theme.error()))));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No internal tasks", 9));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(" " + sortLabel("NAME", "name"), 
sortStyle("name"))),
+                        Cell.from(Span.styled(sortLabel("STATUS", "status"), 
sortStyle("status"))),
+                        rightCell(sortLabel("ATTEMPTS", "attempts"), 8, 
sortStyle("attempts")),
+                        rightCell("DELAY", 8, Style.EMPTY.bold()),
+                        Cell.from(Span.styled(sortLabel("ELAPSED", "elapsed"), 
sortStyle("elapsed"))),
+                        Cell.from(Span.styled("FIRST", Style.EMPTY.bold())),
+                        Cell.from(Span.styled("LAST", Style.EMPTY.bold())),
+                        Cell.from(Span.styled("NEXT", Style.EMPTY.bold())),
+                        Cell.from(Span.styled("ERROR", Style.EMPTY.bold()))))
+                .widths(
+                        Constraint.length(30),
+                        Constraint.length(12),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(10),
+                        Constraint.length(10),
+                        Constraint.length(10),
+                        Constraint.length(10),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Internal Tasks ").build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private int sortTask(InternalTaskInfo a, InternalTaskInfo b) {
+        int result = switch (sort) {
+            case "status" -> {
+                String sa = a.status != null ? a.status : "";
+                String sb = b.status != null ? b.status : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "attempts" -> Long.compare(b.attempts, a.attempts);
+            case "elapsed" -> Long.compare(b.elapsed, a.elapsed);
+            default -> { // "name"
+                String na = a.name != null ? a.name : "";
+                String nb = b.name != null ? b.name : "";
+                yield na.compareToIgnoreCase(nb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static Style statusStyle(String status) {
+        if (status == null) {
+            return Style.EMPTY;
+        }
+        return switch (status) {
+            case "Active" -> Theme.success();
+            case "Completed" -> Theme.info();
+            case "Failed", "Exhausted" -> Theme.error();
+            default -> Style.EMPTY;
+        };
+    }
+
+    private static String formatNext(long nextTime) {
+        if (nextTime <= 0) {
+            return "";
+        }
+        long age = nextTime - System.currentTimeMillis();
+        return TimeUtils.printDuration(age, true);
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.internalTasks.isEmpty()) {
+            return null;
+        }
+        List<InternalTaskInfo> sorted = new ArrayList<>(info.internalTasks);
+        sorted.sort(this::sortTask);
+        List<String> items = sorted.stream().map(t -> t.name != null ? t.name 
: "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Internal Tasks");
+    }
+
+    @Override
+    public String description() {
+        return "Background task registry (reconnections, retries)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Internal Tasks
+
+                Internal tasks are background operations that Camel components 
schedule
+                for retry and reconnection purposes. For example, when a JMS 
or SMPP
+                connection drops, the component registers an internal task 
that periodically
+                attempts to reconnect.
+
+                Tasks self-register when they start running and are removed 
when they
+                complete, fail permanently, or exhaust their retry budget. The 
table
+                shows only **currently active** tasks.
+
+                ## Table Columns
+
+                - **NAME** — Descriptive name of the task (e.g., connection 
target or component)
+                - **STATUS** — Current state: `Active` (running), `Completed` 
(finished successfully), `Failed` (gave up), `Exhausted` (retry budget spent)
+                - **ATTEMPTS** — Number of retry attempts made so far
+                - **DELAY** — Current delay between attempts in milliseconds
+                - **ELAPSED** — Total time since the task started
+                - **FIRST** — When the first attempt was made (ago)
+                - **LAST** — When the last attempt was made (ago)
+                - **NEXT** — Time until the next attempt
+                - **ERROR** — Last error message, if any
+
+                ## When Tasks Appear
+
+                You will typically see internal tasks when:
+                - A messaging broker connection is lost and the consumer is 
reconnecting
+                - A leader election is in progress (e.g., camel-master)
+                - A component is retrying a failed initialization
+
+                An empty table means all connections and background operations 
are healthy.
+
+                ## Keys
+
+                - `Up/Down` — select task
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Internal Tasks");
+        JsonArray rows = new JsonArray();
+        List<InternalTaskInfo> sorted = new ArrayList<>(info.internalTasks);
+        sorted.sort(this::sortTask);
+        for (InternalTaskInfo ti : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("name", ti.name);
+            row.put("status", ti.status);
+            row.put("attempts", ti.attempts);
+            row.put("delay", ti.delay);
+            row.put("elapsed", ti.elapsed);
+            row.put("firstTime", ti.firstTime);
+            row.put("lastTime", ti.lastTime);
+            row.put("nextTime", ti.nextTime);
+            row.put("error", ti.error);
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.internalTasks.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index e7a725f2049a..37ce90e54c47 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -460,6 +460,28 @@ final class StatusParser {
             }
         }
 
+        // Parse internal tasks
+        JsonObject internalTasksObj = (JsonObject) root.get("internal-tasks");
+        if (internalTasksObj != null) {
+            JsonArray taskArr = (JsonArray) internalTasksObj.get("tasks");
+            if (taskArr != null) {
+                for (Object t : taskArr) {
+                    JsonObject tj = (JsonObject) t;
+                    InternalTaskInfo ti = new InternalTaskInfo();
+                    ti.name = tj.getString("name");
+                    ti.status = tj.getString("status");
+                    ti.attempts = tj.getLongOrDefault("attempts", 0);
+                    ti.delay = tj.getLongOrDefault("delay", 0);
+                    ti.elapsed = tj.getLongOrDefault("elapsed", 0);
+                    ti.firstTime = tj.getLongOrDefault("firstTime", 0);
+                    ti.lastTime = tj.getLongOrDefault("lastTime", 0);
+                    ti.nextTime = tj.getLongOrDefault("nextTime", 0);
+                    ti.error = tj.getString("error");
+                    info.internalTasks.add(ti);
+                }
+            }
+        }
+
         // Parse circuit breakers: resilience4j, fault-tolerance, core
         parseCbSection(root, "resilience4j", info);
         parseCbSection(root, "fault-tolerance", info);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 3d667e06667a..803f174b7f97 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -103,6 +103,7 @@ class TabRegistry {
     private KafkaTab kafkaTab;
     private DataSourceTab dataSourceTab;
     private SqlQueryTab sqlQueryTab;
+    private InternalTasksTab internalTasksTab;
     private SqlTraceTab sqlTraceTab;
 
     private MonitorTab activeMoreTab;
@@ -129,6 +130,7 @@ class TabRegistry {
         heapHistogramTab = new HeapHistogramTab(ctx);
         memoryLeakTab = new MemoryLeakTab(ctx);
         sqlQueryTab = new SqlQueryTab(ctx);
+        internalTasksTab = new InternalTasksTab(ctx);
         sqlTraceTab = new SqlTraceTab(ctx);
         endpointsTab = new EndpointsTab(ctx, dataService.metrics());
         networkTab = new NetworkTab(ctx, dataService.metrics());
@@ -195,6 +197,10 @@ class TabRegistry {
                         TuiIcons.TAB_NETWORK, "Network Services", "&Network 
Services", networkTab, "Observability",
                         List.of(), info -> !info.services.isEmpty()),
                 new MoreTab(TuiIcons.TAB_EVENTS, "Events", "&Exchange Events", 
eventTab, "Observability"),
+                new MoreTab(
+                        TuiIcons.TAB_INTERNAL_TASKS, "Internal Tasks", 
"In&ternal Tasks", internalTasksTab,
+                        "Observability",
+                        List.of(), info -> !info.internalTasks.isEmpty()),
                 new MoreTab(
                         TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", spansTab, 
"Observability",
                         List.of("opentelemetry")),
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 32d95332f8e4..6c9c186ce06f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -150,6 +150,7 @@ final class TuiIcons {
     static final String TAB_THREADS = "🧵";
     static final String TAB_PRODUCERS = "📤";
     static final String TAB_EVENTS = "📣";
+    static final String TAB_INTERNAL_TASKS = "🔩";
     static final String TAB_ROUTE_CONTROLLER = "🚦";
 
     /** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link 
TabRegistry#TAB_MORE} (in order). */
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 6122563554f2..91ce742833c6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,7 +82,7 @@ class TabRegistryTest {
 
     @Test
     void moreTabsHasTwentySevenEntries() {
-        assertEquals(28, registry.moreTabs().size());
+        assertEquals(29, registry.moreTabs().size());
     }
 
     @Test
@@ -108,8 +108,8 @@ class TabRegistryTest {
         // MORE_SHORTCUTS array carried before the MoreTab refactor. A label 
edit that repoints a key must fail here.
         List<Character> shortcuts = 
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
         assertEquals(
-                List.of('B', 'C', 'H', 'I', 'P', 'R', 'C', 'H', 'M', 'N', 'E', 
'O', 'J', 'K', 'Q', 'Q', 'C', 'M', 'M', 'M',
-                        'P', 'S', 'T', 'B', 'C', 'C', 'V', 'M'),
+                List.of('B', 'C', 'H', 'I', 'P', 'R', 'C', 'H', 'M', 'N', 'E', 
'T', 'O', 'J', 'K', 'Q', 'Q', 'C', 'M', 'M',
+                        'M', 'P', 'S', 'T', 'B', 'C', 'C', 'V', 'M'),
                 shortcuts, "More tab shortcut letters must match the 
historical sequence");
     }
 

Reply via email to