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 d8023329b947 CAMEL-23870: Add heap histogram dev console, TUI panel, 
CLI command, and MCP tool (#24364)
d8023329b947 is described below

commit d8023329b9474e80c588f9fe0c74959d0721b340
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 1 18:16:06 2026 +0200

    CAMEL-23870: Add heap histogram dev console, TUI panel, CLI command, and 
MCP tool (#24364)
    
    * CAMEL-23870: Add HeapHistogram dev console and TUI panel
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23870: Add camel cmd heap-histogram CLI command
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: use Locale.US consistently across TUI and CLI for number formatting
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: compute heap histogram totals from visible entries only
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: add detail panel to heap histogram tab with package summary and 
JAR origin
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: reset table selection when heap histogram filter changes
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: move heap histogram summary into table title
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: update heap histogram F1 help text and MCP description
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23870: add generated docs and metadata for heap-histogram command
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23870: add heap histogram MCP tool for camel ask
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23870: add generated doc page for heap-histogram CLI command
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23870: add generated catalog files for heap-histogram dev console
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * chore: fix TUI render tests to match CSS theme styles
    
    Tests were asserting pre-CSS-theme colors (YELLOW for hint keys, GREEN
    for success) but the CSS engine resolves to BLACK and LIGHT_GREEN.
    Also fixed renderNoSelection tests to match current prompt text.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles.properties   |   1 +
 .../camel/catalog/dev-consoles/heap-histogram.json |  15 +
 .../console/HeapHistogramDevConsoleConfigurer.java |  63 ++
 .../apache/camel/dev-console/heap-histogram.json   |  15 +
 ...ache.camel.impl.console.HeapHistogramDevConsole |   2 +
 .../org/apache/camel/dev-console/heap-histogram    |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../impl/console/HeapHistogramDevConsole.java      | 178 ++++++
 .../camel-jbang-cmd-heap-histogram.adoc            |  29 +
 .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc |   1 +
 .../camel/cli/connector/LocalCliConnector.java     |  14 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../core/commands/action/CamelHeapHistogram.java   | 243 ++++++++
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  10 +
 .../dsl/jbang/core/commands/tui/FilesBrowser.java  |   4 +-
 .../jbang/core/commands/tui/HeapHistogramTab.java  | 664 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/McpFacade.java     |   8 +-
 .../dsl/jbang/core/commands/tui/PopupManager.java  |  26 +-
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |  24 +-
 .../dsl/jbang/core/commands/tui/TapeRecorder.java  |   8 +-
 .../commands/tui/MonitorContextRenderTest.java     |  32 +-
 .../core/commands/tui/OverviewTabRenderTest.java   |   4 +-
 23 files changed, 1288 insertions(+), 60 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index aff1673d5525..b714441d0ead 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -20,6 +20,7 @@ gcp-secrets
 groovy
 hashicorp-secrets
 health
+heap-histogram
 inflight
 internal-tasks
 java-security
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-histogram.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-histogram.json
new file mode 100644
index 000000000000..af0809c2bbf5
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/heap-histogram.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "heap-histogram",
+    "title": "Heap Histogram",
+    "description": "Displays class-level heap memory usage",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.HeapHistogramDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapHistogramDevConsoleConfigurer.java
 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapHistogramDevConsoleConfigurer.java
new file mode 100644
index 000000000000..707bd8e5f6c7
--- /dev/null
+++ 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapHistogramDevConsoleConfigurer.java
@@ -0,0 +1,63 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.impl.console;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.impl.console.HeapHistogramDevConsole;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class HeapHistogramDevConsoleConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter {
+
+    private static final Map<String, Object> ALL_OPTIONS;
+    static {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("CamelContext", org.apache.camel.CamelContext.class);
+        ALL_OPTIONS = map;
+    }
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.impl.console.HeapHistogramDevConsole target = 
(org.apache.camel.impl.console.HeapHistogramDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": target.setCamelContext(property(camelContext, 
org.apache.camel.CamelContext.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getAllOptions(Object target) {
+        return ALL_OPTIONS;
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return org.apache.camel.CamelContext.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.impl.console.HeapHistogramDevConsole target = 
(org.apache.camel.impl.console.HeapHistogramDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return target.getCamelContext();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-histogram.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-histogram.json
new file mode 100644
index 000000000000..af0809c2bbf5
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-histogram.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "heap-histogram",
+    "title": "Heap Histogram",
+    "description": "Displays class-level heap memory usage",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.HeapHistogramDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapHistogramDevConsole
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapHistogramDevConsole
new file mode 100644
index 000000000000..6059841ac7de
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapHistogramDevConsole
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.HeapHistogramDevConsoleConfigurer
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-histogram
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-histogram
new file mode 100644
index 000000000000..2254ff32042b
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-histogram
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.HeapHistogramDevConsole
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index fe5d76fe5112..c429fc6982f4 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source sql-query 
sql-trace startup-recorder system-properties thread top trace transformers 
type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health heap-histogram inflight 
internal-tasks java-security jvm log memory message-history processor producer 
properties receive reload rest rest-spec route route-controller route-dump 
route-group route-structure route-topology send service simple-language source 
sql-query sql-trace startup-recorder system-properties thread top trace 
transformers type-converters variables
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.21.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapHistogramDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapHistogramDevConsole.java
new file mode 100644
index 000000000000..80487b3d5ba9
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapHistogramDevConsole.java
@@ -0,0 +1,178 @@
+/*
+ * 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.impl.console;
+
+import java.lang.management.ManagementFactory;
+import java.util.Map;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "heap-histogram", displayName = "Heap Histogram", 
description = "Displays class-level heap memory usage")
+@Configurer(extended = true)
+public class HeapHistogramDevConsole extends AbstractDevConsole {
+
+    public HeapHistogramDevConsole() {
+        super("jvm", "heap-histogram", "Heap Histogram", "Displays class-level 
heap memory usage");
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        String histogram = invokeGcClassHistogram();
+        if (histogram == null) {
+            return "Heap histogram not available (DiagnosticCommand MBean not 
found)";
+        }
+        int limit = getLimit(options);
+        if (limit > 0) {
+            return truncateText(histogram, limit);
+        }
+        return histogram;
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        String histogram = invokeGcClassHistogram();
+        if (histogram == null) {
+            root.put("classes", new JsonArray());
+            root.put("totalInstances", 0);
+            root.put("totalBytes", 0);
+            return root;
+        }
+
+        int limit = getLimit(options);
+        return parseHistogram(histogram, limit);
+    }
+
+    private static int getLimit(Map<String, Object> options) {
+        Object val = options.get("limit");
+        if (val != null) {
+            try {
+                return Integer.parseInt(val.toString());
+            } catch (NumberFormatException e) {
+                // use default
+            }
+        }
+        return 100;
+    }
+
+    private static String invokeGcClassHistogram() {
+        try {
+            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+            ObjectName name = new 
ObjectName("com.sun.management:type=DiagnosticCommand");
+            String result = (String) server.invoke(
+                    name, "gcClassHistogram",
+                    new Object[] { null },
+                    new String[] { String[].class.getName() });
+            return result;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static JsonObject parseHistogram(String histogram, int limit) {
+        JsonObject root = new JsonObject();
+        JsonArray arr = new JsonArray();
+        root.put("classes", arr);
+
+        long totalInstances = 0;
+        long totalBytes = 0;
+        int count = 0;
+
+        String[] lines = histogram.split("\n");
+        for (String line : lines) {
+            line = line.trim();
+            if (line.isEmpty() || line.startsWith("-") || 
line.startsWith("num")) {
+                continue;
+            }
+            if (line.startsWith("Total")) {
+                String[] parts = line.split("\\s+");
+                if (parts.length >= 3) {
+                    totalInstances = parseLong(parts[1]);
+                    totalBytes = parseLong(parts[2]);
+                }
+                continue;
+            }
+
+            String[] parts = line.split("\\s+");
+            if (parts.length < 4) {
+                continue;
+            }
+
+            // parts[0]=num: parts[1]=#instances parts[2]=#bytes parts[3]=class
+            String numStr = parts[0].replace(":", "");
+            int num = (int) parseLong(numStr);
+            long instances = parseLong(parts[1]);
+            long bytes = parseLong(parts[2]);
+            String className = parts[3];
+
+            if (limit > 0 && count >= limit) {
+                continue;
+            }
+
+            JsonObject jo = new JsonObject();
+            jo.put("num", num);
+            jo.put("instances", instances);
+            jo.put("bytes", bytes);
+            jo.put("className", className);
+            arr.add(jo);
+            count++;
+        }
+
+        root.put("totalInstances", totalInstances);
+        root.put("totalBytes", totalBytes);
+
+        return root;
+    }
+
+    private static long parseLong(String s) {
+        try {
+            return Long.parseLong(s);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    private static String truncateText(String histogram, int limit) {
+        StringBuilder sb = new StringBuilder();
+        int count = 0;
+        String[] lines = histogram.split("\n");
+        for (String line : lines) {
+            String trimmed = line.trim();
+            if (trimmed.isEmpty() || trimmed.startsWith("-") || 
trimmed.startsWith("num")) {
+                sb.append(line).append('\n');
+                continue;
+            }
+            if (trimmed.startsWith("Total")) {
+                sb.append(line).append('\n');
+                continue;
+            }
+            if (count < limit) {
+                sb.append(line).append('\n');
+                count++;
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-histogram.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-histogram.adoc
new file mode 100644
index 000000000000..f36a78e95cdf
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-histogram.adoc
@@ -0,0 +1,29 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd heap-histogram
+
+Display class-level heap memory usage in a running Camel integration
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd heap-histogram [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--filter` | Filter class names (use all to include all classes) | all | 
String
+| `--sort` | Sort by bytes, instances, or className | bytes | String
+| `--top` | Show only the top N classes | 50 | int
+| `--watch` | Execute periodically and showing output fullscreen |  | boolean
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
index 4e261d317b2d..c5cfec7993db 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
@@ -22,6 +22,7 @@ camel cmd [options]
 | 
xref:jbang-commands/camel-jbang-cmd-disable-processor.adoc[disable-processor] | 
Disable Camel processor
 | xref:jbang-commands/camel-jbang-cmd-enable-processor.adoc[enable-processor] 
| Enable Camel processor
 | xref:jbang-commands/camel-jbang-cmd-gc.adoc[gc] | Trigger Java Memory 
Garbage Collector
+| xref:jbang-commands/camel-jbang-cmd-heap-histogram.adoc[heap-histogram] | 
Display class-level heap memory usage in a running Camel integration
 | xref:jbang-commands/camel-jbang-cmd-load.adoc[load] | Loads new source files 
into an existing Camel
 | xref:jbang-commands/camel-jbang-cmd-logger.adoc[logger] | List or change 
logging levels
 | xref:jbang-commands/camel-jbang-cmd-receive.adoc[receive] | Receive and dump 
messages from remote endpoints
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index d8d562fb77a9..2e336aec1cf7 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -335,6 +335,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionJvmTask();
             } else if ("thread-dump".equals(action)) {
                 doActionThreadDumpTask();
+            } else if ("heap-histogram".equals(action)) {
+                doActionHeapHistogramTask();
             } else if ("top-processors".equals(action)) {
                 doActionTopProcessorsTask();
             } else if ("source".equals(action)) {
@@ -836,6 +838,18 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionHeapHistogramTask() throws IOException {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("heap-histogram");
+        if (dc != null) {
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionKafkaTask() throws IOException {
         DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("kafka");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 9a8c2a2680a7..0a05c64223ab 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 86f19a1abd67..c09e343495f5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -110,6 +110,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("disable-processor", new 
CommandLine(new CamelProcessorDisableAction(this)))
                         .addSubcommand("enable-processor", new CommandLine(new 
CamelProcessorEnableAction(this)))
                         .addSubcommand("gc", new CommandLine(new 
CamelGCAction(this)))
+                        .addSubcommand("heap-histogram", new CommandLine(new 
CamelHeapHistogram(this)))
                         .addSubcommand("load", new CommandLine(new 
CamelLoadAction(this)))
                         .addSubcommand("logger", new CommandLine(new 
LoggerAction(this)))
                         .addSubcommand("receive", new CommandLine(new 
CamelReceiveAction(this)))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
new file mode 100644
index 000000000000..c58ae90c67d5
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogram.java
@@ -0,0 +1,243 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "heap-histogram", description = "Display class-level heap 
memory usage in a running Camel integration",
+         sortOptions = false, showDefaultValues = true,
+         footer = {
+                 "%nExamples:",
+                 "  camel cmd heap-histogram",
+                 "  camel cmd heap-histogram --sort instances",
+                 "  camel cmd heap-histogram --filter camel --top 30" })
+public class CamelHeapHistogram extends ActionWatchCommand {
+
+    public static class SortCompletionCandidates implements Iterable<String> {
+
+        public SortCompletionCandidates() {
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return List.of("bytes", "instances", "className").iterator();
+        }
+    }
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--sort" }, completionCandidates = 
SortCompletionCandidates.class,
+                        description = "Sort by bytes, instances, or 
className", defaultValue = "bytes")
+    String sort;
+
+    @CommandLine.Option(names = { "--top" },
+                        description = "Show only the top N classes", 
defaultValue = "50")
+    int top;
+
+    @CommandLine.Option(names = { "--filter" },
+                        description = "Filter class names (use all to include 
all classes)", defaultValue = "all")
+    String filter;
+
+    private volatile long pid;
+
+    public CamelHeapHistogram(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doWatchCall() throws Exception {
+        List<Row> rows = new ArrayList<>();
+
+        List<Long> pids = findPids(name);
+        if (pids.isEmpty()) {
+            return 1;
+        } else if (pids.size() > 1) {
+            printer().println("Name or pid " + name + " matches " + pids.size()
+                              + " running Camel integrations. Specify a name 
or PID that matches exactly one.");
+            return 1;
+        }
+
+        this.pid = pids.get(0);
+
+        Path outputFile = getOutputFile(Long.toString(pid));
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "heap-histogram");
+        Path f = getActionFile(Long.toString(pid));
+        try {
+            Files.writeString(f, root.toJson());
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            long totalInstances = jo.getLongOrDefault("totalInstances", 0);
+            long totalBytes = jo.getLongOrDefault("totalBytes", 0);
+
+            JsonArray arr = (JsonArray) jo.get("classes");
+            if (arr != null) {
+                for (int i = 0; i < arr.size(); i++) {
+                    JsonObject jc = (JsonObject) arr.get(i);
+
+                    Row row = new Row();
+                    row.num = jc.getIntegerOrDefault("num", 0);
+                    row.className = jc.getString("className");
+                    row.instances = jc.getLongOrDefault("instances", 0);
+                    row.bytes = jc.getLongOrDefault("bytes", 0);
+
+                    if (!matchesFilter(row)) {
+                        continue;
+                    }
+
+                    rows.add(row);
+                }
+            }
+
+            rows.sort(this::sortRow);
+
+            if (top > 0 && rows.size() > top) {
+                rows = rows.subList(0, top);
+            }
+
+            if (watch) {
+                clearScreen();
+            }
+            if (!rows.isEmpty()) {
+                long visibleInstances = 0;
+                long visibleBytes = 0;
+                for (Row r : rows) {
+                    visibleInstances += r.instances;
+                    visibleBytes += r.bytes;
+                }
+                printer().printf("PID: %s\tClasses: %d\tInstances: %s\tBytes: 
%s%n",
+                        pid, rows.size(),
+                        formatNumber(visibleInstances), 
formatBytes(visibleBytes));
+                printTable(rows);
+            }
+        } else {
+            printer().println("Response from running Camel with PID " + pid + 
" not received within 10 seconds");
+            return 1;
+        }
+
+        PathUtils.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    private boolean matchesFilter(Row row) {
+        if ("all".equalsIgnoreCase(filter)) {
+            return true;
+        }
+        if (row.className == null) {
+            return false;
+        }
+        if ("non-jdk".equalsIgnoreCase(filter)) {
+            return !row.className.startsWith("java.")
+                    && !row.className.startsWith("javax.")
+                    && !row.className.startsWith("jdk.")
+                    && !row.className.startsWith("sun.")
+                    && !row.className.startsWith("com.sun.")
+                    && !row.className.startsWith("[");
+        }
+        return row.className.toLowerCase().contains(filter.toLowerCase());
+    }
+
+    protected void printTable(List<Row> rows) {
+        int tw = terminalWidth();
+        int fixedWidth = 6 + 14 + 14;
+        int borderOverhead = TerminalWidthHelper.noBorderOverhead(4);
+        int nameWidth = TerminalWidthHelper.flexWidth(tw, fixedWidth, 
borderOverhead, 30, 80);
+
+        printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, 
Arrays.asList(
+                new 
Column().header("#").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> Integer.toString(r.num)),
+                new Column().header("CLASS 
NAME").dataAlign(HorizontalAlign.LEFT)
+                        .maxWidth(nameWidth, OverflowBehaviour.ELLIPSIS_LEFT)
+                        .with(r -> r.className),
+                new 
Column().header("INSTANCES").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> formatNumber(r.instances)),
+                new 
Column().header("BYTES").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> formatBytes(r.bytes)))));
+    }
+
+    protected int sortRow(Row o1, Row o2) {
+        String s = sort;
+        int negate = 1;
+        if (s.startsWith("-")) {
+            s = s.substring(1);
+            negate = -1;
+        }
+        switch (s) {
+            case "instances":
+                return Long.compare(o2.instances, o1.instances) * negate;
+            case "className":
+                return (o1.className != null
+                        ? o1.className.compareToIgnoreCase(o2.className != 
null ? o2.className : "")
+                        : 0)
+                       * negate;
+            default:
+                return Long.compare(o2.bytes, o1.bytes) * negate;
+        }
+    }
+
+    protected JsonObject waitForOutputFile(Path outputFile) {
+        return getJsonObject(outputFile);
+    }
+
+    static String formatBytes(long bytes) {
+        if (bytes < 1024) {
+            return bytes + " B";
+        } else if (bytes < 1024 * 1024) {
+            return String.format(java.util.Locale.US, "%.1f KB", bytes / 
1024.0);
+        } else if (bytes < 1024L * 1024 * 1024) {
+            return String.format(java.util.Locale.US, "%.1f MB", bytes / 
(1024.0 * 1024));
+        } else {
+            return String.format(java.util.Locale.US, "%.1f GB", bytes / 
(1024.0 * 1024 * 1024));
+        }
+    }
+
+    static String formatNumber(long num) {
+        return String.format(java.util.Locale.US, "%,d", num);
+    }
+
+    private static class Row {
+        int num;
+        String className;
+        long instances;
+        long bytes;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 8c4eace1783a..3f86c4ecd9a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -317,6 +317,16 @@ public class RuntimeTools {
         return runtimeService.executeAction(p.pid(), "thread-dump", null);
     }
 
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
+          description = """
+                  Get a class-level heap histogram showing instance counts and 
byte usage per class. \
+                  Useful for diagnosing memory leaks and understanding which 
classes dominate heap usage.""")
+    public JsonObject camel_runtime_heap_histogram(
+            @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
+        RuntimeService.ProcessInfo p = 
runtimeService.findSingleProcess(nameOrPid);
+        return runtimeService.executeAction(p.pid(), "heap-histogram", null);
+    }
+
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the message history trace of the last completed 
exchange. \
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
index adcfe23c7a30..51062724af1b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
@@ -312,7 +312,7 @@ class FilesBrowser {
 
     static String fileType(Path path) {
         String name = path.getFileName().toString();
-        String lower = name.toLowerCase(Locale.ROOT);
+        String lower = name.toLowerCase(Locale.US);
         if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) 
{
             return "camel";
         }
@@ -336,7 +336,7 @@ class FilesBrowser {
 
     private static String fileEmoji(Path path) {
         String name = path.getFileName().toString();
-        String lower = name.toLowerCase(Locale.ROOT);
+        String lower = name.toLowerCase(Locale.US);
         if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) 
{
             return "πŸͺ";
         }
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
new file mode 100644
index 000000000000..1ceec94648a1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -0,0 +1,664 @@
+/*
+ * 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.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class HeapHistogramTab implements MonitorTab {
+
+    private static final String[] SORT_COLUMNS = { "className", "instances", 
"bytes" };
+    private static final String[] FILTER_LABELS = { "all", "non-jdk", "camel" 
};
+
+    private final MonitorContext ctx;
+    private final TableState tableState = new TableState();
+    private final AtomicBoolean loading = new AtomicBoolean(false);
+
+    private String sort = "bytes";
+    private int sortIndex = 2;
+    private boolean sortReversed;
+    private int filter;
+    private List<HeapEntry> allEntries = Collections.emptyList();
+    private long totalInstances;
+    private long totalBytes;
+    private String lastPid;
+    private List<ClasspathTab.JarEntry> classpathEntries = 
Collections.emptyList();
+
+    HeapHistogramTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    @Override
+    public void onTabSelected() {
+        String pid = ctx.selectedPid;
+        if (pid != null && !pid.equals(lastPid)) {
+            lastPid = pid;
+            allEntries = Collections.emptyList();
+            classpathEntries = Collections.emptyList();
+        }
+        if (allEntries.isEmpty()) {
+            loadHeapHistogram();
+        }
+    }
+
+    @Override
+    public void onIntegrationChanged() {
+        allEntries = Collections.emptyList();
+        classpathEntries = Collections.emptyList();
+        lastPid = null;
+        loadHeapHistogram();
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isChar('s')) {
+            sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+            sort = SORT_COLUMNS[sortIndex];
+            sortReversed = false;
+            return true;
+        }
+        if (ke.isChar('S')) {
+            sortReversed = !sortReversed;
+            return true;
+        }
+        if (ke.isCharIgnoreCase('f')) {
+            filter = (filter + 1) % FILTER_LABELS.length;
+            tableState.select(0);
+            return true;
+        }
+        if (ke.isKey(KeyCode.F5)) {
+            loadHeapHistogram();
+            return true;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            for (int i = 0; i < 20 && tableState.selected() != null && 
tableState.selected() > 0; i++) {
+                tableState.selectPrevious();
+            }
+            return true;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            List<HeapEntry> visible = sortedEntries();
+            for (int i = 0; i < 20; i++) {
+                tableState.selectNext(visible.size());
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        tableState.selectPrevious();
+    }
+
+    @Override
+    public void navigateDown() {
+        List<HeapEntry> visible = sortedEntries();
+        tableState.selectNext(visible.size());
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        if (loading.get() && allEntries.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(
+                                    Line.from(Span.styled(" Loading heap 
histogram...", Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Heap Histogram ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        List<HeapEntry> visible = sortedEntries();
+
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.percentage(60), Constraint.fill())
+                .split(area);
+        renderTable(frame, chunks.get(0), visible);
+        renderDetail(frame, chunks.get(1), visible);
+    }
+
+    private void renderTable(Frame frame, Rect area, List<HeapEntry> visible) {
+        List<Row> rows = new ArrayList<>();
+        for (HeapEntry e : visible) {
+            rows.add(Row.from(
+                    rightCell(String.valueOf(e.num), 6),
+                    Cell.from(Span.styled(e.className != null ? e.className : 
"", Style.EMPTY.fg(Color.CYAN))),
+                    rightCell(formatNumber(e.instances), 14),
+                    rightCell(formatBytes(e.bytes), 14)));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(Span.styled("No data", Style.EMPTY.dim())),
+                    Cell.from(""), Cell.from(""), Cell.from("")));
+        }
+
+        long visibleInstances = 0;
+        long visibleBytes = 0;
+        for (HeapEntry e : visible) {
+            visibleInstances += e.instances;
+            visibleBytes += e.bytes;
+        }
+
+        String title = String.format(" Heap Histogram [%d] instances:%s 
bytes:%s sort:%s filter:%s ",
+                visible.size(), formatNumber(visibleInstances), 
formatBytes(visibleBytes),
+                sort, FILTER_LABELS[filter]);
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        rightCell("#", 6, Style.EMPTY.bold()),
+                        Cell.from(Span.styled(sortLabel("CLASS NAME", 
"className"), sortStyle("className"))),
+                        rightCell(sortLabel("INSTANCES", "instances"), 14, 
sortStyle("instances")),
+                        rightCell(sortLabel("BYTES", "bytes"), 14, 
sortStyle("bytes"))))
+                .widths(
+                        Constraint.length(6),
+                        Constraint.fill(),
+                        Constraint.length(14),
+                        Constraint.length(14))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private void renderDetail(Frame frame, Rect area, List<HeapEntry> visible) 
{
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= visible.size()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled(" Select a 
class to see details", Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        HeapEntry entry = visible.get(sel);
+        String className = entry.className != null ? entry.className : "";
+        String pkg = extractPackage(className);
+
+        List<Line> lines = new ArrayList<>();
+
+        // Class info
+        lines.add(Line.from(
+                Span.styled("  Class:      ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.styled(className, Style.EMPTY.fg(Color.CYAN))));
+        lines.add(Line.from(
+                Span.styled("  Package:    ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.styled(pkg.isEmpty() ? "(none)" : pkg, 
Style.EMPTY.fg(Color.WHITE))));
+        lines.add(Line.from(
+                Span.styled("  Instances:  ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.styled(formatNumber(entry.instances), 
Style.EMPTY.fg(Color.WHITE)),
+                Span.styled("          Bytes: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.styled(formatBytes(entry.bytes), 
Style.EMPTY.fg(Color.WHITE))));
+
+        // Package summary
+        if (!pkg.isEmpty()) {
+            long pkgInstances = 0;
+            long pkgBytes = 0;
+            int pkgClasses = 0;
+            for (HeapEntry e : allEntries) {
+                if (e.className != null && 
extractPackage(e.className).equals(pkg)) {
+                    pkgInstances += e.instances;
+                    pkgBytes += e.bytes;
+                    pkgClasses++;
+                }
+            }
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled("  Package Summary ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.styled("(" + pkg + ")", Style.EMPTY.dim())));
+            lines.add(Line.from(
+                    Span.styled("    Classes: ", Style.EMPTY.fg(Color.YELLOW)),
+                    Span.styled(formatNumber(pkgClasses), 
Style.EMPTY.fg(Color.WHITE)),
+                    Span.styled("     Instances: ", 
Style.EMPTY.fg(Color.YELLOW)),
+                    Span.styled(formatNumber(pkgInstances), 
Style.EMPTY.fg(Color.WHITE)),
+                    Span.styled("     Bytes: ", Style.EMPTY.fg(Color.YELLOW)),
+                    Span.styled(formatBytes(pkgBytes), 
Style.EMPTY.fg(Color.WHITE))));
+        }
+
+        // JAR info
+        ClasspathTab.JarEntry jar = findJar(className);
+        if (jar != null) {
+            lines.add(Line.from(Span.raw("")));
+            if (jar.groupId() != null) {
+                lines.add(Line.from(
+                        Span.styled("  JAR:        ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                        Span.styled(jar.groupId() + ":" + jar.artifactId() + 
":" + jar.version(),
+                                Style.EMPTY.fg(Color.GREEN))));
+            } else {
+                lines.add(Line.from(
+                        Span.styled("  JAR:        ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                        Span.styled(jar.display(), 
Style.EMPTY.fg(Color.GREEN))));
+            }
+            if (jar.fullPath() != null) {
+                String path = jar.fullPath();
+                int maxW = area.width() - 18;
+                if (maxW > 0 && path.length() > maxW) {
+                    path = "..." + path.substring(path.length() - maxW + 3);
+                }
+                lines.add(Line.from(
+                        Span.styled("                ", Style.EMPTY),
+                        Span.styled(path, Style.EMPTY.dim())));
+            }
+        } else if (isBuiltinClass(className)) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled("  JAR:        ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.styled("JDK (built-in)", 
Style.EMPTY.fg(Color.GREEN))));
+        }
+
+        String title = " Detail: " + TuiHelper.truncate(className, 
area.width() - 14) + " ";
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(lines))
+                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+                        .build(),
+                area);
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        hint(spans, "Esc", "back");
+        hint(spans, "s", "sort");
+        hint(spans, "f", "filter [" + FILTER_LABELS[filter] + "]");
+        hintLast(spans, "F5", "refresh");
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        List<HeapEntry> visible = sortedEntries();
+        if (visible.isEmpty()) {
+            return null;
+        }
+        List<String> items = visible.stream().map(e -> e.className != null ? 
e.className : "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Heap Histogram");
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        List<HeapEntry> entries = sortedEntries();
+        if (entries.isEmpty()) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Heap Histogram");
+        JsonArray rows = new JsonArray();
+        for (HeapEntry e : entries) {
+            JsonObject row = new JsonObject();
+            row.put("num", e.num);
+            row.put("className", e.className);
+            row.put("instances", e.instances);
+            row.put("bytes", e.bytes);
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", allEntries.size());
+        result.put("totalInstances", totalInstances);
+        result.put("totalBytes", totalBytes);
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Heap Histogram
+
+                The Heap Histogram tab shows class-level memory usage in the 
JVM heap,
+                similar to `jcmd <pid> GC.class_histogram`. Each row 
represents a class
+                with the number of live instances and total bytes consumed.
+
+                This is useful for diagnosing memory leaks, finding unexpected 
object
+                retention, and understanding which classes dominate heap usage.
+
+                ## Table Columns
+
+                - **#** β€” Rank by bytes (from the raw JVM histogram)
+                - **CLASS NAME** β€” Fully qualified class name. Array types use 
JVM notation (e.g., `[B` = byte array, `[Ljava.lang.Object;` = Object array)
+                - **INSTANCES** β€” Number of live instances of this class on 
the heap
+                - **BYTES** β€” Total bytes consumed by all instances of this 
class
+
+                The title bar shows total classes, instances, and bytes for 
the current filter.
+
+                ## Detail Panel
+
+                The detail panel below the table shows additional context for 
the selected class:
+
+                - **Class** β€” Full class name, package, instance count and 
bytes
+                - **Package Summary** β€” Total classes, instances, and bytes 
for all classes in the same package
+                - **JAR** β€” The Maven artifact (groupId:artifactId:version) 
and file path of the JAR containing the class. JDK classes show "JDK (built-in)"
+
+                ## Filter Modes
+
+                - **all** (default) β€” Show all classes
+                - **non-jdk** β€” Exclude JDK classes (java.*, javax.*, jdk.*, 
sun.*, com.sun.*, arrays)
+                - **camel** β€” Show only classes from `org.apache.camel` 
packages
+
+                ## What To Look For
+
+                - **Large byte counts at the top**: Normal for byte arrays and 
char arrays β€” these back Strings and buffers
+                - **Unexpected classes with high counts**: May indicate a 
memory leak
+                - **Growing instance counts on refresh**: Press F5 repeatedly 
to spot classes whose counts keep growing
+                - **Package summary**: Use the detail panel to see total 
memory for an entire package
+                - **JAR origin**: Identify which dependency owns the 
memory-heavy classes
+
+                ## Keys
+
+                | Key | Action |
+                |-----|--------|
+                | Up/Down | Select class |
+                | s | Cycle sort column (className, instances, bytes) |
+                | S | Reverse sort order |
+                | f | Toggle filter (all / non-jdk / camel) |
+                | F5 | Refresh heap histogram |
+                | PgUp/PgDn | Scroll by page |
+                | Esc | Back |
+                """;
+    }
+
+    private List<HeapEntry> sortedEntries() {
+        List<HeapEntry> result = new ArrayList<>();
+        for (HeapEntry e : allEntries) {
+            if (filter == 1 && isJavaClass(e)) {
+                continue;
+            }
+            if (filter == 2 && !isCamelClass(e)) {
+                continue;
+            }
+            result.add(e);
+        }
+        result.sort((a, b) -> {
+            int cmp = switch (sort) {
+                case "instances" -> Long.compare(b.instances, a.instances);
+                case "className" -> compareStr(a.className, b.className);
+                default -> Long.compare(b.bytes, a.bytes);
+            };
+            return sortReversed ? -cmp : cmp;
+        });
+        return result;
+    }
+
+    private static boolean isJavaClass(HeapEntry e) {
+        if (e.className == null) {
+            return false;
+        }
+        return e.className.startsWith("java.")
+                || e.className.startsWith("javax.")
+                || e.className.startsWith("jdk.")
+                || e.className.startsWith("sun.")
+                || e.className.startsWith("com.sun.")
+                || e.className.startsWith("[");
+    }
+
+    private static boolean isCamelClass(HeapEntry e) {
+        return e.className != null && e.className.contains("org.apache.camel");
+    }
+
+    private static boolean isBuiltinClass(String className) {
+        if (className == null || className.isEmpty()) {
+            return false;
+        }
+        return className.startsWith("java.")
+                || className.startsWith("javax.")
+                || className.startsWith("jdk.")
+                || className.startsWith("sun.")
+                || className.startsWith("com.sun.")
+                || className.startsWith("[");
+    }
+
+    private static String extractPackage(String className) {
+        if (className == null || className.isEmpty()) {
+            return "";
+        }
+        if (className.startsWith("[")) {
+            // array type β€” extract element class if object array
+            int idx = className.lastIndexOf('L');
+            if (idx >= 0) {
+                String element = className.substring(idx + 1).replace(";", "");
+                int dot = element.lastIndexOf('.');
+                return dot >= 0 ? element.substring(0, dot) : "";
+            }
+            return "";
+        }
+        int dot = className.lastIndexOf('.');
+        return dot >= 0 ? className.substring(0, dot) : "";
+    }
+
+    private ClasspathTab.JarEntry findJar(String className) {
+        if (classpathEntries.isEmpty() || className == null || 
className.isEmpty()) {
+            return null;
+        }
+        String pkg = extractPackage(className);
+        if (pkg.isEmpty()) {
+            return null;
+        }
+        // try progressively shorter prefixes of the package against groupId
+        ClasspathTab.JarEntry best = null;
+        int bestLen = -1;
+        for (ClasspathTab.JarEntry jar : classpathEntries) {
+            if (jar.groupId() == null) {
+                continue;
+            }
+            String gid = jar.groupId();
+            if (pkg.startsWith(gid) && gid.length() > bestLen) {
+                best = jar;
+                bestLen = gid.length();
+            }
+        }
+        return best;
+    }
+
+    private static int compareStr(String a, String b) {
+        if (a == null && b == null) {
+            return 0;
+        }
+        if (a == null) {
+            return -1;
+        }
+        if (b == null) {
+            return 1;
+        }
+        return a.compareToIgnoreCase(b);
+    }
+
+    private String sortLabel(String label, String column) {
+        return MonitorContext.sortLabel(label, column, sort, sortReversed);
+    }
+
+    private Style sortStyle(String column) {
+        return MonitorContext.sortStyle(column, sort);
+    }
+
+    private void loadHeapHistogram() {
+        if (ctx.selectedPid == null || ctx.runner == null) {
+            return;
+        }
+        if (!loading.compareAndSet(false, true)) {
+            return;
+        }
+        String pid = ctx.selectedPid;
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                loadHeapHistogramInBackground(pid);
+            } finally {
+                loading.set(false);
+            }
+        });
+    }
+
+    private void loadHeapHistogramInBackground(String pid) {
+        Path outputFile = ctx.getOutputFile(pid);
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "heap-histogram");
+
+        Path actionFile = ctx.getActionFile(pid);
+        PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+        JsonObject jo = pollJsonResponse(outputFile, 5000);
+        PathUtils.deleteFile(outputFile);
+
+        if (jo == null) {
+            return;
+        }
+
+        long ti = jo.getLongOrDefault("totalInstances", 0);
+        long tb = jo.getLongOrDefault("totalBytes", 0);
+
+        JsonArray arr = (JsonArray) jo.get("classes");
+        if (arr == null) {
+            return;
+        }
+
+        List<HeapEntry> result = new ArrayList<>();
+        for (int i = 0; i < arr.size(); i++) {
+            JsonObject ej = (JsonObject) arr.get(i);
+            HeapEntry entry = new HeapEntry();
+            entry.num = ej.getIntegerOrDefault("num", 0);
+            entry.className = ej.getString("className");
+            entry.instances = ej.getLongOrDefault("instances", 0);
+            entry.bytes = ej.getLongOrDefault("bytes", 0);
+            result.add(entry);
+        }
+
+        // load classpath if not already loaded
+        List<ClasspathTab.JarEntry> cpEntries = classpathEntries;
+        if (cpEntries.isEmpty()) {
+            cpEntries = loadClasspathEntries(pid);
+        }
+
+        List<ClasspathTab.JarEntry> finalCp = cpEntries;
+        if (ctx.runner != null) {
+            ctx.runner.runOnRenderThread(() -> {
+                allEntries = result;
+                totalInstances = ti;
+                totalBytes = tb;
+                classpathEntries = finalCp;
+                lastPid = pid;
+            });
+        }
+    }
+
+    private List<ClasspathTab.JarEntry> loadClasspathEntries(String pid) {
+        Path outputFile = ctx.getOutputFile(pid);
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject action = new JsonObject();
+        action.put("action", "jvm");
+
+        Path actionFile = ctx.getActionFile(pid);
+        PathUtils.writeTextSafely(action.toJson(), actionFile);
+
+        JsonObject response = pollJsonResponse(outputFile, 5000);
+        PathUtils.deleteFile(outputFile);
+
+        if (response == null) {
+            return Collections.emptyList();
+        }
+
+        Object cp = response.get("classpath");
+        List<String> paths = new ArrayList<>();
+        if (cp instanceof JsonArray jArr) {
+            for (Object item : jArr) {
+                paths.add(String.valueOf(item));
+            }
+        }
+
+        if (paths.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<ClasspathTab.JarEntry> parsed = new ArrayList<>();
+        for (String path : paths) {
+            parsed.add(ClasspathTab.parseJarEntry(path));
+        }
+        return parsed;
+    }
+
+    static String formatBytes(long bytes) {
+        if (bytes < 1024) {
+            return bytes + " B";
+        } else if (bytes < 1024 * 1024) {
+            return String.format(java.util.Locale.US, "%.1f KB", bytes / 
1024.0);
+        } else if (bytes < 1024L * 1024 * 1024) {
+            return String.format(java.util.Locale.US, "%.1f MB", bytes / 
(1024.0 * 1024));
+        } else {
+            return String.format(java.util.Locale.US, "%.1f GB", bytes / 
(1024.0 * 1024 * 1024));
+        }
+    }
+
+    static String formatNumber(long num) {
+        return String.format(java.util.Locale.US, "%,d", num);
+    }
+
+    static class HeapEntry {
+        int num;
+        String className;
+        long instances;
+        long bytes;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 674c4a3f0f3e..11096df9a093 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -90,8 +90,8 @@ class McpFacade {
 
     static final String[] MORE_TAB_NAMES = {
             "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
-            "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "SQL 
Query", "SQL Trace", "Spans", "Process",
-            "Startup", "Threads"
+            "Consumers", "DataSource", "Heap Histogram", "Inflight", "Memory", 
"Metrics", "SQL Query", "SQL Trace",
+            "Spans", "Process", "Startup", "Threads"
     };
 
     static final Map<String, String> TAB_DESCRIPTIONS = Map.ofEntries(
@@ -111,6 +111,8 @@ class McpFacade {
             Map.entry("Configuration", "Application configuration properties"),
             Map.entry("Consumers", "Consumer statistics (polling and 
event-driven consumers)"),
             Map.entry("DataSource", "JDBC DataSource pool statistics (active, 
idle, max connections)"),
+            Map.entry("Heap Histogram",
+                    "Class-level heap memory analysis showing instance counts, 
byte usage, package summary, and JAR origin per class"),
             Map.entry("Inflight", "Currently in-flight exchanges being 
processed"),
             Map.entry("Memory", "JVM memory usage (heap/non-heap), GC stats, 
and thread counts"),
             Map.entry("Metrics", "Micrometer metrics (counters, gauges, 
timers, distribution summaries)"),
@@ -354,7 +356,7 @@ class McpFacade {
 
         KeyModifiers mods = KeyModifiers.of(ctrl, false, shift);
 
-        KeyCode code = switch (remainder.toLowerCase(Locale.ROOT)) {
+        KeyCode code = switch (remainder.toLowerCase(Locale.US)) {
             case "enter", "return" -> KeyCode.ENTER;
             case "esc", "escape" -> KeyCode.ESCAPE;
             case "tab" -> KeyCode.TAB;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index ba1c87e859ed..7687999001cc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -176,7 +176,7 @@ class PopupManager {
             return true;
         }
         if (ke.isDown()) {
-            morePopupState.selectNext(16);
+            morePopupState.selectNext(17);
             return true;
         }
         int shortcutSel = morePopupShortcut(ke);
@@ -239,7 +239,7 @@ class PopupManager {
 
     void renderMorePopup(Frame frame, Rect area) {
         int popupW = 22;
-        int popupH = 18;
+        int popupH = 19;
         // Position just below the "0 Moreβ–Ύ" tab label
         int dividerW = CharWidth.of(" | ");
         int tabBarX = 0;
@@ -268,6 +268,7 @@ class PopupManager {
                 ListItem.from(Line.from(Span.raw("  Confi"), Span.styled("g", 
keyStyle), Span.raw("uration"))),
                 ListItem.from(Line.from(Span.raw("  Co"), Span.styled("n", 
keyStyle), Span.raw("sumers"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("D", 
keyStyle), Span.raw("ataSource"))),
+                ListItem.from(Line.from(Span.raw("  "), Span.styled("H", 
keyStyle), Span.raw("eap Histogram"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("I", 
keyStyle), Span.raw("nflight"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("M", 
keyStyle), Span.raw("emory"))),
                 ListItem.from(Line.from(Span.raw("  M"), Span.styled("e", 
keyStyle), Span.raw("trics"))),
@@ -397,33 +398,36 @@ class PopupManager {
         if (ke.isChar('d')) {
             return 6;
         }
-        if (ke.isChar('i')) {
+        if (ke.isChar('h')) {
             return 7;
         }
-        if (ke.isChar('m')) {
+        if (ke.isChar('i')) {
             return 8;
         }
-        if (ke.isChar('e')) {
+        if (ke.isChar('m')) {
             return 9;
         }
-        if (ke.isChar('q')) {
+        if (ke.isChar('e')) {
             return 10;
         }
-        if (ke.isChar('r')) {
+        if (ke.isChar('q')) {
             return 11;
         }
-        if (ke.isChar('o')) {
+        if (ke.isChar('r')) {
             return 12;
         }
-        if (ke.isChar('p')) {
+        if (ke.isChar('o')) {
             return 13;
         }
-        if (ke.isChar('s')) {
+        if (ke.isChar('p')) {
             return 14;
         }
-        if (ke.isChar('t')) {
+        if (ke.isChar('s')) {
             return 15;
         }
+        if (ke.isChar('t')) {
+            return 16;
+        }
         return -1;
     }
 
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 4fb2bf453383..e0cf19d7e1f7 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
@@ -86,6 +86,7 @@ class TabRegistry {
     private ProcessTab processTab;
     private OverviewTab overviewTab;
     private DataSourceTab dataSourceTab;
+    private HeapHistogramTab heapHistogramTab;
     private SqlQueryTab sqlQueryTab;
     private SqlTraceTab sqlTraceTab;
 
@@ -105,6 +106,7 @@ class TabRegistry {
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
         dataSourceTab = new DataSourceTab(ctx);
+        heapHistogramTab = new HeapHistogramTab(ctx);
         sqlQueryTab = new SqlQueryTab(ctx);
         sqlTraceTab = new SqlTraceTab(ctx);
         endpointsTab = new EndpointsTab(ctx, dataService.metrics());
@@ -129,7 +131,7 @@ class TabRegistry {
                 resetIntegrationTabState);
 
         sqlTraceTab.setEditSqlAction(sql -> {
-            selectMoreTab(10); // switch to SQL Query tab
+            selectMoreTab(11); // switch to SQL Query tab
             sqlQueryTab.setInputValue("sql", sql);
         });
     }
@@ -216,15 +218,16 @@ class TabRegistry {
             case 4 -> configurationTab;
             case 5 -> consumersTab;
             case 6 -> dataSourceTab;
-            case 7 -> inflightTab;
-            case 8 -> memoryTab;
-            case 9 -> metricsTab;
-            case 10 -> sqlQueryTab;
-            case 11 -> sqlTraceTab;
-            case 12 -> spansTab;
-            case 13 -> processTab;
-            case 14 -> startupTab;
-            case 15 -> threadsTab;
+            case 7 -> heapHistogramTab;
+            case 8 -> inflightTab;
+            case 9 -> memoryTab;
+            case 10 -> metricsTab;
+            case 11 -> sqlQueryTab;
+            case 12 -> sqlTraceTab;
+            case 13 -> spansTab;
+            case 14 -> processTab;
+            case 15 -> startupTab;
+            case 16 -> threadsTab;
             default -> null;
         };
         if (activeMoreTab != null) {
@@ -247,6 +250,7 @@ class TabRegistry {
         configurationTab.onIntegrationChanged();
         consumersTab.onIntegrationChanged();
         dataSourceTab.onIntegrationChanged();
+        heapHistogramTab.onIntegrationChanged();
         sqlQueryTab.onIntegrationChanged();
         sqlTraceTab.onIntegrationChanged();
         circuitBreakerTab.onIntegrationChanged();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
index 9fe3b138075a..b0b33dc3574e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
@@ -170,7 +170,7 @@ class TapeRecorder {
                 default -> null;
             };
         }
-        String lower = key.toLowerCase(Locale.ROOT);
+        String lower = key.toLowerCase(Locale.US);
 
         if (lower.startsWith("ctrl+")) {
             return "Ctrl+" + key.substring(5);
@@ -189,14 +189,14 @@ class TapeRecorder {
         }
 
         if (lower.matches("f\\d{1,2}")) {
-            return key.toUpperCase(Locale.ROOT);
+            return key.toUpperCase(Locale.US);
         }
 
         return null;
     }
 
     private static String capitalize(String s) {
-        return s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1);
+        return s.substring(0, 1).toUpperCase(Locale.US) + s.substring(1);
     }
 
     static String formatSleep(long ms) {
@@ -205,7 +205,7 @@ class TapeRecorder {
         }
         if (ms >= 1000) {
             double seconds = ms / 1000.0;
-            String formatted = String.format(Locale.ROOT, "%.1f", seconds);
+            String formatted = String.format(Locale.US, "%.1f", seconds);
             return formatted + "s";
         }
         long rounded = (ms / 100) * 100;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
index 875b1fd43e93..4ba1170134ba 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
@@ -46,8 +46,6 @@ class MonitorContextRenderTest {
         String rendered = HealthTabRenderTest.bufferToString(buffer);
         assertTrue(rendered.contains("No integration selected"),
                 "Should render 'No integration selected' in the block title");
-        assertTrue(rendered.contains("Select an integration"),
-                "Should render the selection prompt text");
     }
 
     @Test
@@ -59,38 +57,21 @@ class MonitorContextRenderTest {
         MonitorContext.renderNoSelection(frame, area);
 
         String rendered = HealthTabRenderTest.bufferToString(buffer);
-        // The block title should appear in the buffer
         assertTrue(rendered.contains("No integration selected"),
                 "Should render the block title");
-        // The prompt text should appear somewhere in the buffer
-        assertTrue(rendered.contains("Select an integration"),
-                "Should render the prompt text");
     }
 
     @Test
-    void renderNoSelectionPromptIsDimmed() {
-        Rect area = new Rect(0, 0, 80, 10);
+    void renderNoSelectionShowsHintKeys() {
+        Rect area = new Rect(0, 0, 80, 20);
         Buffer buffer = Buffer.empty(area);
         Frame frame = Frame.forTesting(buffer);
 
         MonitorContext.renderNoSelection(frame, area);
 
-        // Check that the "Select" text cell has DIM modifier
-        boolean foundDim = false;
-        for (int y = 0; y < buffer.height(); y++) {
-            for (int x = 0; x < buffer.width(); x++) {
-                var cell = buffer.get(x, y);
-                if ("S".equals(cell.symbol()) && 
cell.style().effectiveModifiers()
-                        .contains(dev.tamboui.style.Modifier.DIM)) {
-                    foundDim = true;
-                    break;
-                }
-            }
-            if (foundDim) {
-                break;
-            }
-        }
-        assertTrue(foundDim, "Prompt text should use DIM modifier");
+        String rendered = HealthTabRenderTest.bufferToString(buffer);
+        assertTrue(rendered.contains("Overview"), "Should render the Overview 
hint");
+        assertTrue(rendered.contains("Help"), "Should render the Help hint");
     }
 
     @Test
@@ -104,13 +85,12 @@ class MonitorContextRenderTest {
     }
 
     @Test
-    void hintKeyUsesYellowBoldStyle() {
+    void hintKeyIsBoldStyled() {
         List<Span> spans = new ArrayList<>();
         MonitorContext.hint(spans, "s", "sort");
 
         Span keySpan = spans.get(0);
         assertTrue(keySpan.style().fg().isPresent(), "Key span should have a 
foreground color");
-        assertEquals(Color.YELLOW, keySpan.style().fg().get(), "Key span 
should be YELLOW");
         
assertTrue(keySpan.style().effectiveModifiers().contains(dev.tamboui.style.Modifier.BOLD),
                 "Key span should be BOLD");
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
index 22bc0fbd92e0..dd2f32e67a56 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
@@ -114,8 +114,8 @@ class OverviewTabRenderTest {
         Frame frame = Frame.forTesting(buffer);
         tab.render(frame, area);
 
-        boolean foundGreen = TuiTestHelper.findCellWithColor(buffer, "R", 
Color.GREEN);
-        assertTrue(foundGreen, "Running status should be rendered in GREEN");
+        boolean foundGreen = TuiTestHelper.findCellWithColor(buffer, "R", 
Color.LIGHT_GREEN);
+        assertTrue(foundGreen, "Running status should be rendered in 
LIGHT_GREEN");
     }
 
     @Test

Reply via email to