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 830313e04aa6 CAMEL-24759: camel-jbang - TUI: drop the TamboUI 0.5.0 
workarounds and align the web terminal's emoji widths (#26612)
830313e04aa6 is described below

commit 830313e04aa6848bc92af89a1ead8ec0409c2d6b
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 19 11:55:03 2026 +0200

    CAMEL-24759: camel-jbang - TUI: drop the TamboUI 0.5.0 workarounds and 
align the web terminal's emoji widths (#26612)
    
    * CAMEL-24759: camel-jbang - TUI: drop the workarounds that TamboUI 0.5.0 
makes redundant
    
    TuiBackendHelper no longer wraps the explicit JLine backend for
    recording itself: TuiRunner.create does that for explicitly configured
    backends since tamboui/tamboui#418. The recording test now goes through
    createTuiRunner and asserts on the runner's backend instead.
    
    The throughput chart keeps its rate * 100 history intact and converts
    the y-axis label back to msg/s through DualSparkline.yAxisFormatter
    (tamboui/tamboui#396) rather than dividing the data before rendering,
    so sub-1 msg/s rates keep their bar height again. The payload size
    chart gets a compact byte formatter instead of the default 999+ cap.
    
    The plain-emoji icon rule from CAMEL-23818 stays: TamboUI 0.5.0 now
    measures base + VS16 sequences as 2 columns (tamboui/tamboui#388), but
    the xterm.js Unicode 11 width tables used by --web still treat VS16 as
    zero-width, so the comments now give that as the reason.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24759: camel-jbang - TUI: web terminal measures VS16 emoji like 
TamboUI, restoring the CAMEL-23818 icons
    
    The --web frontend used the xterm.js Unicode 11 addon, whose tables
    treat the VS16 variation selector as zero-width, so a text-default
    glyph such as the keyboard or wastebasket took one column in the
    browser while TamboUI 0.5.0 reserves two (tamboui/tamboui#388). Swap
    the addon for @xterm/addon-unicode-graphemes 0.4.0, which promotes
    base + VS16 to a 2-column cell; every icon in TuiIcons now measures
    the same in xterm.js and in TamboUI's CharWidth.
    
    That makes the CAMEL-23818 workaround reversible: the keystrokes,
    tape recording and delete actions get their keyboard, record, stop
    and wastebasket icons back (with VS16, since terminals draw the bare
    glyphs in one column), and the no-VS16 test rule is replaced by one
    that requires those icons to carry VS16 and measure two columns.
    
    Verified in Chrome: the Screen submenu keeps its alignment and moving
    the selection over the VS16 rows leaves no stray characters.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24759: camel-jbang - TUI: size table scrollbars from 
Table.viewportHeight
    
    renderTableScrollbar assumed every table shows area.height() - 3 rows
    (top border, header, bottom border), and the History tab repeated the
    same arithmetic for scrollToSelected. The Ollama tab's request table
    adds a footer row once there are two or more questions, so its
    scrollbar ran one row too long and its thumb was sized for a viewport
    the table did not have.
    
    TamboUI 0.5.0 exposes Table.viewportHeight(Rect) (tamboui/tamboui#420),
    which subtracts the block chrome, header and footer the table was built
    with. The scrollbar helpers now take the Table and use it, and the
    History tab hoists its inline-built tables so the same instance sizes
    both the selection scroll and the scrollbar.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24759: camel-jbang - TUI: show the captured output when the CLI 
executor test fails
    
    productionInvokerRestoresPrinterAndDoesNotQuitMain failed twice today
    with "expected: <0> but was: <1>" and could not be reproduced in eight
    further runs. Exit code 1 is both picocli's code for a command that
    threw and the executor's default when the invoker itself throws, and
    in either case the exception is in the captured output, which the
    assertion discarded. Put it in the failure message so the next
    occurrence says what actually went wrong.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../dsl/jbang/core/commands/tui/AbstractTab.java   | 13 ++-
 .../jbang/core/commands/tui/AbstractTableTab.java  |  5 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  2 +-
 .../dsl/jbang/core/commands/tui/ActivityTab.java   |  2 +-
 .../dsl/jbang/core/commands/tui/BeansTab.java      |  2 +-
 .../dsl/jbang/core/commands/tui/BrowseTab.java     |  4 +-
 .../dsl/jbang/core/commands/tui/CatalogTab.java    |  2 +-
 .../jbang/core/commands/tui/CircuitBreakerTab.java |  2 +-
 .../jbang/core/commands/tui/ConfigurationTab.java  |  2 +-
 .../dsl/jbang/core/commands/tui/ConsumersTab.java  |  2 +-
 .../dsl/jbang/core/commands/tui/CveAuditTab.java   |  2 +-
 .../dsl/jbang/core/commands/tui/DataSourceTab.java |  2 +-
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |  2 +-
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     |  2 +-
 .../dsl/jbang/core/commands/tui/EventTab.java      |  2 +-
 .../dsl/jbang/core/commands/tui/FlowHelper.java    | 30 +++++--
 .../dsl/jbang/core/commands/tui/HealthTab.java     |  2 +-
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 25 +++---
 .../camel/dsl/jbang/core/commands/tui/HttpTab.java |  2 +-
 .../dsl/jbang/core/commands/tui/InflightTab.java   |  2 +-
 .../jbang/core/commands/tui/InternalTasksTab.java  |  2 +-
 .../camel/dsl/jbang/core/commands/tui/JfrTab.java  | 12 +--
 .../dsl/jbang/core/commands/tui/KafkaTab.java      |  2 +-
 .../core/commands/tui/MavenDependenciesTab.java    |  2 +-
 .../dsl/jbang/core/commands/tui/MetricsTab.java    |  2 +-
 .../dsl/jbang/core/commands/tui/NetworkTab.java    |  2 +-
 .../dsl/jbang/core/commands/tui/OllamaTab.java     |  2 +-
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  4 +-
 .../dsl/jbang/core/commands/tui/ProducersTab.java  |  2 +-
 .../core/commands/tui/RouteControllerTab.java      |  2 +-
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  4 +-
 .../dsl/jbang/core/commands/tui/SecretsTab.java    |  2 +-
 .../dsl/jbang/core/commands/tui/SpansTab.java      |  2 +-
 .../dsl/jbang/core/commands/tui/SqlQueryTab.java   |  2 +-
 .../dsl/jbang/core/commands/tui/SqlTraceTab.java   |  2 +-
 .../dsl/jbang/core/commands/tui/ThreadsTab.java    |  2 +-
 .../jbang/core/commands/tui/TransformersTab.java   |  2 +-
 .../jbang/core/commands/tui/TuiBackendHelper.java  | 26 ++----
 .../dsl/jbang/core/commands/tui/TuiIcons.java      | 15 ++--
 .../jbang/core/commands/tui/TypeConvertersTab.java |  2 +-
 .../src/main/resources/tui/web/index.html          | 11 ++-
 .../resources/tui/web/vendor/LICENSE-xterm.txt     |  8 +-
 .../web/vendor/xterm-addon-unicode-graphemes.js    |  2 +
 .../tui/web/vendor/xterm-addon-unicode11.js        |  2 -
 .../commands/tui/AbstractTabScrollbarTest.java     | 98 ++++++++++++++++++++++
 .../commands/tui/AiCliCommandExecutorTest.java     |  3 +-
 .../jbang/core/commands/tui/FlowHelperTest.java    | 57 +++++++++++++
 .../jbang/core/commands/tui/TabRegistryTest.java   |  3 +-
 .../tui/TuiBackendHelperRecordingTest.java         | 41 +++++----
 .../dsl/jbang/core/commands/tui/TuiIconsTest.java  | 19 +++--
 50 files changed, 311 insertions(+), 131 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
index f585f31e2eab..8835eb919743 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
@@ -35,6 +35,7 @@ import dev.tamboui.widgets.scrollbar.Scrollbar;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
 import dev.tamboui.widgets.table.Cell;
 import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 
 abstract class AbstractTab implements MonitorTab {
@@ -118,15 +119,21 @@ abstract class AbstractTab implements MonitorTab {
 
     // ---- Mouse / scrollbar helpers ----
 
+    /**
+     * Draws a scrollbar over the right border of a bordered table with a 
header row, sized to the rows the table
+     * actually shows in {@code tableArea} (so a footer row, as in the Ollama 
tab, shortens it accordingly).
+     */
     protected static void renderTableScrollbar(
-            Frame frame, Rect tableArea, TableState tableState, ScrollbarState 
scrollState, int rowCount) {
-        if (tableArea == null || tableState == null || scrollState == null) {
+            Frame frame, Rect tableArea, Table table, TableState tableState, 
ScrollbarState scrollState,
+            int rowCount) {
+        if (tableArea == null || table == null || tableState == null || 
scrollState == null) {
             return;
         }
-        int visibleRows = tableArea.height() - 3;
+        int visibleRows = table.viewportHeight(tableArea);
         if (visibleRows <= 0 || rowCount <= visibleRows) {
             return;
         }
+        // the data rows start below the top border and the header row
         Rect scrollRect = new Rect(
                 tableArea.x() + tableArea.width() - 1,
                 tableArea.y() + 2,
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java
index b5ca8016e6a8..223193e2fa74 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTableTab.java
@@ -26,6 +26,7 @@ import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
@@ -138,7 +139,7 @@ abstract class AbstractTableTab extends AbstractTab {
         return sortStyle(column, sort);
     }
 
-    protected void renderScrollbar(Frame frame, int rowCount) {
-        renderTableScrollbar(frame, lastTableArea, tableState, 
tableScrollState, rowCount);
+    protected void renderScrollbar(Frame frame, Table table, int rowCount) {
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
tableScrollState, rowCount);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index ca2caa18951d..817decfdaad7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -1062,7 +1062,7 @@ class ActionsPopup {
                 ? TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Hide Keystrokes")
                 : TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Show Keystrokes");
         String tapeLabel = tapeRecordingActive.get()
-                ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording")
+                ? TuiIcons.menuItem(TuiIcons.STOP_RECORD, "Stop Tape 
Recording")
                 : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording");
         List<ListItem> items = new ArrayList<>();
         items.add(ListItem.from("  .."));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
index c239cbc80378..0cfbdca0549c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
@@ -204,7 +204,7 @@ class ActivityTab extends AbstractTableTab {
 
         lastTableArea = chunks.get(1);
         frame.renderStatefulWidget(table, chunks.get(1), tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
 
         if (showDetail) {
             renderDetail(frame, chunks.get(2), selectedEntry);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
index 8cc616a9b008..616f940e3b32 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
@@ -247,7 +247,7 @@ class BeansTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, visible.size());
+        renderScrollbar(frame, table, visible.size());
     }
 
     private void renderDetail(Frame frame, Rect area, List<BeanData> visible) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
index 5f2bea9282df..d084bd52277a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
@@ -344,7 +344,7 @@ class BrowseTab extends AbstractTab {
 
         lastEndpointTableArea = area;
         frame.renderStatefulWidget(table, area, endpointTableState);
-        renderTableScrollbar(frame, lastEndpointTableArea, endpointTableState, 
endpointTableScrollState,
+        renderTableScrollbar(frame, lastEndpointTableArea, table, 
endpointTableState, endpointTableScrollState,
                 sorted.size());
     }
 
@@ -406,7 +406,7 @@ class BrowseTab extends AbstractTab {
 
         lastMessageTableArea = area;
         frame.renderStatefulWidget(table, area, messageTableState);
-        renderTableScrollbar(frame, lastMessageTableArea, messageTableState, 
messageTableScrollState,
+        renderTableScrollbar(frame, lastMessageTableArea, table, 
messageTableState, messageTableScrollState,
                 messages.size());
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
index ac9a44d9f4af..6580567d705a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -302,7 +302,7 @@ class CatalogTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private void renderDetail(Frame frame, Rect area, List<CatalogEntry> 
sorted) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
index ec8e392f9dae..00b88047dd7d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
@@ -162,7 +162,7 @@ class CircuitBreakerTab extends AbstractTableTab {
 
         lastTableArea = chunks.get(0);
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
-        renderScrollbar(frame, info.circuitBreakers.size());
+        renderScrollbar(frame, table, info.circuitBreakers.size());
 
         if (showDiagram) {
             renderDiagram(frame, chunks.get(1), selectedCb, info.pid);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index daa2ce39ff52..f7b07bde5676 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -212,7 +212,7 @@ class ConfigurationTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, props.size());
+        renderScrollbar(frame, table, props.size());
     }
 
     private void renderDetail(Frame frame, Rect area, List<ConfigProperty> 
props) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index 4f50f9acd942..d3af7c3d1dc8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -124,7 +124,7 @@ class ConsumersTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private int sortConsumer(ConsumerInfo a, ConsumerInfo b) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
index fb69a634eb07..b161da30df73 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
@@ -277,7 +277,7 @@ class CveAuditTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, visible.size());
+        renderScrollbar(frame, table, visible.size());
     }
 
     private void renderDetail(Frame frame, Rect area) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
index 5cb3b625cf13..c07ce9b04c51 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
@@ -133,7 +133,7 @@ class DataSourceTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private int sortDataSource(DataSourceInfo a, DataSourceInfo b) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index a51aa87b522d..c17bdd22c20b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -336,7 +336,7 @@ class EndpointsTab extends AbstractTableTab {
 
         lastTableArea = chunks.get(0);
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
-        renderScrollbar(frame, sortedEndpoints.size());
+        renderScrollbar(frame, table, sortedEndpoints.size());
 
         if (showPanel && panelMode == PANEL_DETAIL) {
             hSplit.clearBorderPos();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index ecb0850e3be3..129c4ac47d5d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -389,7 +389,7 @@ class ErrorsTab extends AbstractTableTab {
 
         lastTableArea = chunks.get(0);
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
-        renderScrollbar(frame, filteredSize());
+        renderScrollbar(frame, table, filteredSize());
 
         if (showDetail) {
             renderDetail(frame, chunks.get(1), selectedError);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
index d609b289dae4..04b0b0686beb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
@@ -114,7 +114,7 @@ class EventTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private int sortEvent(EventInfo a, EventInfo b) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
index fb3a9c1b08cf..91064f51d6e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
@@ -126,11 +126,6 @@ final class FlowHelper {
         }
         long curIn = inArr[renderPoints - 1];
         long curOut = outArr[renderPoints - 1];
-        // scale down from internal precision (rate * THROUGHPUT_SCALE) to 
actual msg/s for y-axis
-        for (int i = 0; i < renderPoints; i++) {
-            inArr[i] = Math.round((double) inArr[i] / 
MetricsCollector.THROUGHPUT_SCALE);
-            outArr[i] = Math.round((double) outArr[i] / 
MetricsCollector.THROUGHPUT_SCALE);
-        }
 
         List<Span> titleSpans = new ArrayList<>();
         if (chartLabel != null) {
@@ -156,6 +151,9 @@ final class FlowHelper {
                 .topStyle(Theme.success())
                 .bottomStyle(Style.EMPTY.fg(Theme.accent()))
                 .showYAxis(true)
+                // the data stays scaled by THROUGHPUT_SCALE so sub-1 msg/s 
rates keep their bar height; the axis
+                // label converts back to msg/s like the title does
+                .yAxisFormatter(MetricsCollector::formatThroughput)
                 .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
                         "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -198,6 +196,7 @@ final class FlowHelper {
                 .topStyle(Theme.label())
                 .bottomStyle(Theme.notice())
                 .showYAxis(true)
+                .yAxisFormatter(FlowHelper::compactSize)
                 .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
                         "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -221,6 +220,27 @@ final class FlowHelper {
         }
     }
 
+    /**
+     * Formats a byte count into at most four characters for the sparkline 
y-axis, where {@link #sizeToString(long)}
+     * would not fit: {@code 512}, {@code 1.5K}, {@code 12K}, {@code 1.2M}.
+     */
+    static String compactSize(long size) {
+        if (size < 1000) {
+            return String.valueOf(Math.max(0, size));
+        }
+        double kb = size / 1024.0;
+        if (kb < 10) {
+            return String.format(Locale.US, "%.1fK", kb);
+        } else if (kb < 999.5) {
+            return Math.round(kb) + "K";
+        }
+        double mb = kb / 1024.0;
+        if (mb < 10) {
+            return String.format(Locale.US, "%.1fM", mb);
+        }
+        return Math.round(mb) + "M";
+    }
+
     private static long unbox(Long value) {
         return value != null ? value : 0L;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
index eda5db9588fc..fdf62c8560cf 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
@@ -138,7 +138,7 @@ class HealthTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, healthChecks.size());
+        renderScrollbar(frame, table, healthChecks.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 04803bff807a..8a3b5d0fcbf3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -1298,10 +1298,9 @@ class HistoryTab extends AbstractTab {
                 .build();
 
         lastTraceTableArea = area;
-        int traceVisibleRows = Math.max(0, area.height() - 3);
-        traceTableState.scrollToSelected(traceVisibleRows, rows);
+        traceTableState.scrollToSelected(table.viewportHeight(area), rows);
         frame.renderStatefulWidget(table, area, traceTableState);
-        renderTableScrollbar(frame, lastTraceTableArea, traceTableState, 
tableScrollState,
+        renderTableScrollbar(frame, lastTraceTableArea, table, 
traceTableState, tableScrollState,
                 traceSortedExchangeIds.size());
     }
 
@@ -1332,12 +1331,10 @@ class HistoryTab extends AbstractTab {
         Style tableHighlight = showFocus && detailFocused ? 
Theme.selectionBg().dim() : Theme.selectionBg();
         lastTraceStepArea = chunks.get(0);
         detailSplit.setBorderPos(chunks.get(1).y());
-        int stepVisibleRows = Math.max(0, chunks.get(0).height() - 3);
-        traceStepTableState.scrollToSelected(stepVisibleRows, rows);
-        frame.renderStatefulWidget(
-                buildStepTable(rows, stepTitle, showDescription, 
tableBorderStyle, tableHighlight),
-                chunks.get(0), traceStepTableState);
-        renderTableScrollbar(frame, lastTraceStepArea, traceStepTableState, 
traceStepScrollState,
+        Table stepTable = buildStepTable(rows, stepTitle, showDescription, 
tableBorderStyle, tableHighlight);
+        
traceStepTableState.scrollToSelected(stepTable.viewportHeight(chunks.get(0)), 
rows);
+        frame.renderStatefulWidget(stepTable, chunks.get(0), 
traceStepTableState);
+        renderTableScrollbar(frame, lastTraceStepArea, stepTable, 
traceStepTableState, traceStepScrollState,
                 steps.size());
 
         if (showWaterfall) {
@@ -1618,12 +1615,10 @@ class HistoryTab extends AbstractTab {
         Style tableHighlight = showFocus && detailFocused ? 
Theme.selectionBg().dim() : Theme.selectionBg();
         lastHistoryTableArea = chunks.get(0);
         vSplit.setBorderPos(chunks.get(1).y());
-        int histVisibleRows = Math.max(0, chunks.get(0).height() - 3);
-        historyTableState.scrollToSelected(histVisibleRows, rows);
-        frame.renderStatefulWidget(
-                buildStepTable(rows, historyTitle, showDescription, 
tableBorderStyle, tableHighlight),
-                chunks.get(0), historyTableState);
-        renderTableScrollbar(frame, lastHistoryTableArea, historyTableState, 
historyTableScrollState,
+        Table historyTable = buildStepTable(rows, historyTitle, 
showDescription, tableBorderStyle, tableHighlight);
+        
historyTableState.scrollToSelected(historyTable.viewportHeight(chunks.get(0)), 
rows);
+        frame.renderStatefulWidget(historyTable, chunks.get(0), 
historyTableState);
+        renderTableScrollbar(frame, lastHistoryTableArea, historyTable, 
historyTableState, historyTableScrollState,
                 current.size());
 
         if (showWaterfall) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 38e319944d9b..b53942844fc5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -417,7 +417,7 @@ class HttpTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, visible.size());
+        renderScrollbar(frame, table, visible.size());
     }
 
     // ---- Detail panel ----
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
index 8b9f2944e514..323bc95e0029 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
@@ -129,7 +129,7 @@ class InflightTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private void renderBrowseDisabled(Frame frame, Rect area) {
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
index 53581b74fcb4..726041335b42 100644
--- 
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
@@ -110,7 +110,7 @@ class InternalTasksTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private int sortTask(InternalTaskInfo a, InternalTaskInfo b) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
index 5d5cfdb54c12..d22fabb37db0 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
@@ -402,7 +402,7 @@ class JfrTab extends AbstractTab {
                 .build();
         lastTableArea = chunks.get(0);
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
-        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
data.size());
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
scrollState, data.size());
 
         Integer selectedRoute = tableState.selected();
         if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < 
data.size()) {
@@ -471,7 +471,7 @@ class JfrTab extends AbstractTab {
                         .title(" Processors [" + routeId + "] (" + data.size() 
+ ") ").build())
                 .build();
         frame.renderStatefulWidget(table, area, processorTableState);
-        renderTableScrollbar(frame, area, processorTableState, 
processorScrollState, data.size());
+        renderTableScrollbar(frame, area, table, processorTableState, 
processorScrollState, data.size());
     }
 
     private void renderProcessorsTable(Frame frame, Rect area) {
@@ -526,7 +526,7 @@ class JfrTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
data.size());
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
scrollState, data.size());
     }
 
     private void renderEndpointsTable(Frame frame, Rect area) {
@@ -577,7 +577,7 @@ class JfrTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
data.size());
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
scrollState, data.size());
     }
 
     private void renderFailuresTable(Frame frame, Rect area) {
@@ -613,7 +613,7 @@ class JfrTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
failureData.size());
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
scrollState, failureData.size());
     }
 
     private void renderRedeliveriesTable(Frame frame, Rect area) {
@@ -646,7 +646,7 @@ class JfrTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
redeliveryData.size());
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
scrollState, redeliveryData.size());
     }
 
     private static String formatMs(double ms) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/KafkaTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/KafkaTab.java
index 6cbfc9289571..28f097df61d9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/KafkaTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/KafkaTab.java
@@ -132,7 +132,7 @@ class KafkaTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private void renderDetail(Frame frame, Rect area, List<KafkaConsumerInfo> 
sorted) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
index 36794c0f88b0..d834970d7ef8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
@@ -311,7 +311,7 @@ class MavenDependenciesTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
index 4de3ac25cba7..88457d728c6d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
@@ -517,7 +517,7 @@ class MetricsTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, lastRowCount);
+        renderScrollbar(frame, table, lastRowCount);
     }
 
     // ---- Raw metrics view ----
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
index 604ead1b54b6..2fb4c1bf71ae 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
@@ -242,7 +242,7 @@ class NetworkTab extends AbstractTableTab {
 
         lastTableArea = chunks.get(0);
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
 
         if (showChart) {
             String selectedUri = null;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
index 5301c2420ef1..4b8de6ce2a8d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
@@ -608,7 +608,7 @@ class OllamaTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderTableScrollbar(frame, area, tableState, scrollState, 
rows.size());
+        renderTableScrollbar(frame, area, table, tableState, scrollState, 
rows.size());
     }
 
     private static final int FIXED_COLUMNS_WIDTH = 10 + 18 + 6 + 6 + 6 + 5 + 8 
+ 8 + 7 + 7 + 12 + 6;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index bb7828f7caf8..81d0e2624080 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -332,7 +332,7 @@ class OverviewTab extends AbstractTab {
         lastTableArea = chunks.get(0);
         TableState renderState = infraFocused ? new TableState() : tableState;
         frame.renderStatefulWidget(table, chunks.get(0), renderState);
-        renderTableScrollbar(frame, lastTableArea, tableState, 
tableScrollState, integrationCount);
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
tableScrollState, integrationCount);
 
         if (infraCount > 0) {
             renderInfraTable(frame, chunks.get(1), infraInfos);
@@ -1034,7 +1034,7 @@ class OverviewTab extends AbstractTab {
         lastInfraTableArea = area;
         TableState renderState = infraFocused ? infraTableState : new 
TableState();
         frame.renderStatefulWidget(infraTable, area, renderState);
-        renderTableScrollbar(frame, area, infraTableState, infraScrollState, 
infraInfos.size());
+        renderTableScrollbar(frame, area, infraTable, infraTableState, 
infraScrollState, infraInfos.size());
     }
 
     private static String extractInfraPort(InfraInfo info) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
index 559eedbf7058..25ec16d59033 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
@@ -97,7 +97,7 @@ class ProducersTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private int sortProducer(ProducerInfo a, ProducerInfo b) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
index cef2806a1813..194a06995e67 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
@@ -147,7 +147,7 @@ class RouteControllerTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     private void renderEmptyState(Frame frame, Rect area, String message, 
String title) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 9b200d9d57f7..27e7f0efeb34 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -568,7 +568,7 @@ class RoutesTab extends AbstractTab {
         lastRouteTableArea = chunks.get(0);
         vSplit.setBorderPos(chunks.get(1).y());
         frame.renderStatefulWidget(routeTable, chunks.get(0), routeTableState);
-        renderTableScrollbar(frame, lastRouteTableArea, routeTableState, 
routeTableScrollState,
+        renderTableScrollbar(frame, lastRouteTableArea, routeTable, 
routeTableState, routeTableScrollState,
                 info.routes.size());
 
         // Bottom panel: processors
@@ -1157,7 +1157,7 @@ class RoutesTab extends AbstractTab {
 
         frame.renderStatefulWidget(table, area, processorTableState);
         int processorRowCount = routeTopMode ? route.processors.size() : 
route.processors.size() + 1;
-        renderTableScrollbar(frame, area, processorTableState, 
processorTableScrollState,
+        renderTableScrollbar(frame, area, table, processorTableState, 
processorTableScrollState,
                 processorRowCount);
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SecretsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SecretsTab.java
index 777ff92e3799..8246fbd94b40 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SecretsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SecretsTab.java
@@ -93,7 +93,7 @@ class SecretsTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
index 8adac52b31ee..717637ba7be4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
@@ -417,7 +417,7 @@ class SpansTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, traceListState);
-        renderTableScrollbar(frame, lastTableArea, traceListState, 
tableScrollState, summaries.size());
+        renderTableScrollbar(frame, lastTableArea, table, traceListState, 
tableScrollState, summaries.size());
     }
 
     private void renderWaterfallView(Frame frame, Rect area) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
index ced4d2fb992d..ebf2055265dc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
@@ -522,7 +522,7 @@ class SqlQueryTab extends AbstractTab {
                 .build();
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderTableScrollbar(frame, lastTableArea, tableState, 
tableScrollState, resultRows.size());
+        renderTableScrollbar(frame, lastTableArea, table, tableState, 
tableScrollState, resultRows.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
index 1ae90a31c4d7..3a70646f0c6e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
@@ -293,7 +293,7 @@ class SqlTraceTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, sorted.size());
+        renderScrollbar(frame, table, sorted.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
index 5a734d3e6952..b404fb19bf1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
@@ -226,7 +226,7 @@ class ThreadsTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, visible.size());
+        renderScrollbar(frame, table, visible.size());
     }
 
     private void renderTrace(Frame frame, Rect area, List<ThreadData> visible) 
{
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
index f19b04153dd1..abb2ad200fda 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TransformersTab.java
@@ -123,7 +123,7 @@ class TransformersTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, visible.size());
+        renderScrollbar(frame, table, visible.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
index 2b5fa294e30f..3a5791ac6e19 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
@@ -17,8 +17,6 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import dev.tamboui.backend.jline3.JLineBackend;
-import dev.tamboui.internal.record.RecordingBackend;
-import dev.tamboui.internal.record.RecordingConfig;
 import dev.tamboui.terminal.Backend;
 import dev.tamboui.tui.TuiConfig;
 import dev.tamboui.tui.TuiRunner;
@@ -40,26 +38,12 @@ final class TuiBackendHelper {
         return createTuiRunner(backend);
     }
 
-    static TuiRunner createTuiRunner(Backend backend) throws Exception {
-        return TuiRunner.create(
-                
TuiConfig.builder().backend(applyRecording(backend)).mouseCapture(true).bracketedPaste(true).build());
-    }
-
     /**
-     * Wraps the backend for Asciinema recording when {@code --record} 
configured the {@code tamboui.record*} system
-     * properties.
-     * <p>
-     * TamboUI normally does this inside {@code BackendFactory.create()}, but 
{@code TuiRunner} only calls that factory
-     * when no explicit backend is configured. Because we must pass an 
explicit backend (see
-     * {@link #createTuiRunner()}), the wrapping has to be done here instead, 
otherwise {@code --record} exits cleanly
-     * without replaying the tape or writing a {@code .cast} file.
+     * Creates the runner for an explicitly built backend. {@code 
TuiRunner.create} wraps it for Asciinema recording
+     * itself when {@code --record} configured the {@code tamboui.record*} 
system properties.
      */
-    static Backend applyRecording(Backend backend) {
-        // Guard on isEnabled() first: load() caches its result process-wide 
and installs a System.out capture
-        if (!RecordingConfig.isEnabled()) {
-            return backend;
-        }
-        RecordingConfig config = RecordingConfig.load();
-        return config != null ? new RecordingBackend(backend, config) : 
backend;
+    static TuiRunner createTuiRunner(Backend backend) throws Exception {
+        return TuiRunner.create(
+                
TuiConfig.builder().backend(backend).mouseCapture(true).bracketedPaste(true).build());
     }
 }
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 554f1e0f6dc2..f3ee3e6a738d 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
@@ -21,8 +21,10 @@ import java.util.List;
 /**
  * Single source of truth for emoji and symbolic icons used across the Camel 
TUI.
  * <p/>
- * Tab/menu icons use plain 2-column-wide emoji without VS16 variation 
selectors (see CAMEL-23818). Doctor and legacy
- * status glyphs may still use mixed-width symbols until migrated.
+ * Tab/menu icons are 2-column emoji. A text-default glyph such as the 
keyboard or the wastebasket must carry the VS16
+ * variation selector (U+FE0F): TamboUI counts the bare glyph as 2 columns 
while terminals draw it in 1, and with VS16
+ * both TamboUI 0.5.0 (tamboui/tamboui#388) and the xterm.js grapheme width 
tables used by {@code --web} agree on 2.
+ * Doctor and legacy status glyphs may still use mixed-width symbols until 
migrated.
  */
 final class TuiIcons {
 
@@ -60,18 +62,17 @@ final class TuiIcons {
     // memo (πŸ“) reads as "edit"; the letters glyph reads as changing the name
     static final String RENAME = "πŸ”€";
     static final String DUPLICATE = "πŸ“‘";
-    // NOTE: the wastebasket emoji (πŸ—‘) is width-ambiguous and TamboUI does not 
align it correctly yet, so use the
-    // cross-mark instead until that is fixed upstream.
-    static final String DELETE = "❌";
+    static final String DELETE = "πŸ—‘οΈ";
 
     // ---- Actions menu ----
     static final String GO_TO = "πŸ”";
     static final String MESSAGE = "πŸ“©";
-    static final String KEYSTROKES = "πŸ”€";
+    static final String KEYSTROKES = "⌨️";
     static final String SLEEP = "πŸ’€";
     static final String STOP = "πŸ›‘";
     static final String QUIT = "πŸšͺ";
-    static final String RECORD = "πŸ”΄";
+    static final String RECORD = "⏺️";
+    static final String STOP_RECORD = "⏹️";
     static final String DOCTOR = "🩺";
     static final String RESET = "πŸ”„";
     static final String CLEAN = "🧹";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
index ccd5eccf8ebb..f16100637995 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
@@ -187,7 +187,7 @@ class TypeConvertersTab extends AbstractTableTab {
 
         lastTableArea = area;
         frame.renderStatefulWidget(table, area, tableState);
-        renderScrollbar(frame, visible.size());
+        renderScrollbar(frame, table, visible.size());
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/index.html 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/index.html
index 4362ef184eaf..c9b009654e7c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/index.html
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/index.html
@@ -143,7 +143,7 @@
 
   <script src="/vendor/xterm.js"></script>
   <script src="/vendor/xterm-addon-fit.js"></script>
-  <script src="/vendor/xterm-addon-unicode11.js"></script>
+  <script src="/vendor/xterm-addon-unicode-graphemes.js"></script>
   <script>
     (function () {
       'use strict';
@@ -199,9 +199,12 @@
 
           fitAddon = new FitAddon.FitAddon();
           term.loadAddon(fitAddon);
-          var unicode11Addon = new Unicode11Addon.Unicode11Addon();
-          term.loadAddon(unicode11Addon);
-          term.unicode.activeVersion = '11';
+          // Unicode 15 width tables with grapheme clustering, so a 
text-default glyph followed by
+          // VS16 (e.g. the keyboard or record icons) takes the same 2 columns 
as in tamboui.
+          // The Unicode 11 addon treats VS16 as zero-width and misaligns 
everything after it.
+          var unicodeGraphemesAddon = new 
UnicodeGraphemesAddon.UnicodeGraphemesAddon();
+          term.loadAddon(unicodeGraphemesAddon);
+          term.unicode.activeVersion = '15-graphemes';
           term.open(document.getElementById('terminal-container'));
 
           function sendInit() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/LICENSE-xterm.txt
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/LICENSE-xterm.txt
index 9cad528cd98c..b849cd9b852c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/LICENSE-xterm.txt
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/LICENSE-xterm.txt
@@ -14,10 +14,12 @@ 
https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit
 
 Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
 
-xterm-addon-unicode11 0.9.0 (xterm-addon-unicode11.js)
-https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11
+xterm-addon-unicode-graphemes 0.4.0 (xterm-addon-unicode-graphemes.js)
+https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes
 
-Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
+Copyright (c) 2023, The xterm.js authors (https://github.com/xtermjs/xterm.js)
+The bundled Unicode property tables are generated from
+https://github.com/PerBothner/unicode-properties (MIT).
 
 All are released under the MIT License:
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/xterm-addon-unicode-graphemes.js
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/xterm-addon-unicode-graphemes.js
new file mode 100644
index 000000000000..b887d1b3f0d2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/xterm-addon-unicode-graphemes.js
@@ -0,0 +1,2 @@
+!function(e,t){"object"==typeof exports&&"object"==typeof 
module?module.exports=t():"function"==typeof 
define&&define.amd?define([],t):"object"==typeof 
exports?exports.UnicodeGraphemesAddon=t():e.UnicodeGraphemesAddon=t()}(globalThis,(()=>(()=>{"use
 strict";var 
e={106:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeGraphemeProvider=void
 0;const r=s(765),n=s(200);class 
i{constructor(e=!0){this.ambiguousCharsAreWide=!1,this.version=e?"15-graphemes":"15",this.handleGraph
 [...]
+//# sourceMappingURL=addon-unicode-graphemes.js.map
\ No newline at end of file
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/xterm-addon-unicode11.js
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/xterm-addon-unicode11.js
deleted file mode 100644
index 941130afb3ad..000000000000
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/vendor/xterm-addon-unicode11.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(e,t){"object"==typeof exports&&"object"==typeof 
module?module.exports=t():"function"==typeof 
define&&define.amd?define([],t):"object"==typeof 
exports?exports.Unicode11Addon=t():e.Unicode11Addon=t()}(globalThis,(()=>(()=>{"use
 strict";var 
e={384:(e,t,s)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV11=void
 0;const 
r=s(765),n=[[768,879],[1155,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1541],[1552,1562],[1564,1564],[1611,1631],[1648,1648
 [...]
-//# sourceMappingURL=addon-unicode11.js.map
\ No newline at end of file
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTabScrollbarTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTabScrollbarTest.java
new file mode 100644
index 000000000000..200e0e6262ce
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTabScrollbarTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.buffer.Buffer;
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+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.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The table scrollbar has to cover exactly the data rows the table shows: a 
footer row (as in the Ollama tab) takes one
+ * row away from the viewport, and a scrollbar sized from the area height 
alone would run over it.
+ */
+class AbstractTabScrollbarTest {
+
+    private static final String THUMB = "β–ˆ";
+    private static final int ROWS = 40;
+
+    @Test
+    void scrollbarEndsOnTheLastDataRowOfAPlainTable() {
+        Rect area = new Rect(0, 0, 30, 12);
+        Buffer buffer = render(area, table(false));
+
+        // top border + header, then 9 data rows, then the bottom border
+        int lastDataRow = area.height() - 2;
+        assertThat(symbol(buffer, lastDataRow)).isEqualTo(THUMB);
+        assertThat(symbol(buffer, area.height() - 1)).isEqualTo("β•―");
+    }
+
+    @Test
+    void scrollbarStopsAboveTheFooterRow() {
+        Rect area = new Rect(0, 0, 30, 12);
+        Buffer buffer = render(area, table(true));
+
+        int footerRow = area.height() - 2;
+        assertThat(symbol(buffer, footerRow - 1)).isEqualTo(THUMB);
+        assertThat(symbol(buffer, footerRow)).as("footer row keeps the 
border").isEqualTo("β”‚");
+    }
+
+    private static Buffer render(Rect area, Table table) {
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        TableState tableState = new TableState();
+        // scrolled to the end, so the thumb sits on the last data row of the 
viewport
+        tableState.selectLast(ROWS);
+        frame.renderStatefulWidget(table, area, tableState);
+        AbstractTab.renderTableScrollbar(frame, area, table, tableState, new 
ScrollbarState(), ROWS);
+        return buffer;
+    }
+
+    private static String symbol(Buffer buffer, int y) {
+        return buffer.get(buffer.area().width() - 1, y).symbol();
+    }
+
+    private static Table table(boolean footer) {
+        List<Row> rows = new ArrayList<>();
+        for (int i = 0; i < ROWS; i++) {
+            rows.add(Row.from(Cell.from("row " + i), Cell.from("value")));
+        }
+        Table.Builder builder = Table.builder()
+                .rows(rows)
+                .header(Row.from(Cell.from("NAME"), Cell.from("VALUE")))
+                .widths(Constraint.length(10), Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).build());
+        if (footer) {
+            builder.footer(Row.from(Cell.from("total"), 
Cell.from(String.valueOf(ROWS))));
+        }
+        return builder.build();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
index c2a80093f542..c59d87960362 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
@@ -149,7 +149,8 @@ class AiCliCommandExecutorTest {
                     .executeAsync(new 
AiCliCommandExecutor.Request(List.of("tui-test"), "camel tui-test"))
                     .get(5, TimeUnit.SECONDS);
 
-            assertEquals(0, result.exitCode());
+            // a non-zero exit carries the exception the command or the 
invoker threw in the output, so show it
+            assertEquals(0, result.exitCode(), () -> "unexpected exit code, 
output was: " + result.output());
             assertEquals("captured command output\n", result.output());
             assertSame(originalPrinter, main.getOut());
         } finally {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelperTest.java
new file mode 100644
index 000000000000..d7daaa72fafd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelperTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class FlowHelperTest {
+
+    @Test
+    void compactSizeFitsTheFourColumnAxisLabel() {
+        Map<Long, String> expected = Map.of(
+                0L, "0",
+                512L, "512",
+                999L, "999",
+                1024L, "1.0K",
+                1536L, "1.5K",
+                12288L, "12K",
+                1023488L, "1.0M", // 999.5 KB rounds up into the megabyte 
range rather than to "1000K"
+                1048576L, "1.0M",
+                1258291L, "1.2M",
+                15728640L, "15M");
+
+        expected.forEach((size, label) -> {
+            assertThat(FlowHelper.compactSize(size)).as("size %d", 
size).isEqualTo(label);
+            assertThat(label.length()).isLessThanOrEqualTo(4);
+        });
+    }
+
+    @Test
+    void throughputAxisLabelConvertsScaledRatesBackToMessagesPerSecond() {
+        // the chart data stays scaled by THROUGHPUT_SCALE, so the axis label 
has to undo the scaling
+        assertThat(MetricsCollector.formatThroughput(0)).isEqualTo("0");
+        assertThat(MetricsCollector.formatThroughput(20)).isEqualTo("0.20");
+        assertThat(MetricsCollector.formatThroughput(150)).isEqualTo("1.5");
+        assertThat(MetricsCollector.formatThroughput(700)).isEqualTo("7.0");
+        assertThat(MetricsCollector.formatThroughput(1500)).isEqualTo("15");
+        
assertThat(MetricsCollector.formatThroughput(1234500)).isEqualTo("12K");
+    }
+}
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 081443e3dcc5..83036314dc37 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
@@ -114,10 +114,9 @@ class TabRegistryTest {
     }
 
     @Test
-    void moreTabIconsAreTwoColumnsWideWithoutVariationSelector() {
+    void moreTabIconsAreTwoColumnsWide() {
         for (TabRegistry.MoreTab mt : registry.moreTabs()) {
             assertEquals(2, CharWidth.of(mt.icon()), "Icon should be 2 
terminal columns wide: " + mt.icon());
-            assertFalse(mt.icon().contains("\uFE0F"), "Icon should not contain 
VS16 variation selector: " + mt.icon());
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
index fa2e495ae974..c95f2a274b38 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
@@ -25,6 +25,7 @@ import dev.tamboui.internal.record.AnsiTerminalCapture;
 import dev.tamboui.layout.Position;
 import dev.tamboui.layout.Size;
 import dev.tamboui.terminal.Backend;
+import dev.tamboui.tui.TuiRunner;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -35,12 +36,12 @@ import static org.assertj.core.api.Assertions.assertThat;
 /**
  * Verifies that {@code camel tui --record} actually engages TamboUI's 
recording backend.
  * <p>
- * TamboUI only wraps a backend for recording inside {@code 
BackendFactory.create()}, and {@code TuiRunner} calls that
- * factory <em>only</em> when no explicit backend is configured. Since 
camel-tui must supply an explicit
- * {@link dev.tamboui.backend.jline3.JLineBackend} (auto-discovery would 
otherwise pick the Aesh backend that is on the
- * classpath for {@code --web}), the recording wrapper is never applied unless 
camel-tui applies it itself. When that
- * wrapping is missing, {@code --record} replays no tape and writes no {@code 
.cast} file, yet still exits cleanly, so
- * only a test like this one catches the regression.
+ * camel-tui must supply an explicit {@link 
dev.tamboui.backend.jline3.JLineBackend} (auto-discovery would otherwise
+ * pick the Aesh backend that is on the classpath for {@code --web}). Before 
TamboUI 0.5.0 only backends created by
+ * TamboUI's own factory were wrapped for recording, so camel-tui wrapped its 
explicit backend itself; since 0.5.0
+ * {@code TuiRunner.create} does that for explicit backends too. When the 
wrapping goes missing, {@code --record}
+ * replays no tape and writes no {@code .cast} file, yet still exits cleanly, 
so only a test like this one catches the
+ * regression.
  */
 // RecordingConfig.load() also reads fps and duration, so every key camel-tui 
sets has to be managed here: a value
 // leaking out of this class would silently reconfigure recording in an 
unrelated test. junit-pioneer clears each key
@@ -65,12 +66,12 @@ class TuiBackendHelperRecordingTest {
     }
 
     @Test
-    void withoutRecordOptionTheBackendIsHandedToTuiRunnerUntouched() {
+    void withoutRecordOptionTheBackendIsHandedToTuiRunnerUntouched() throws 
Exception {
         Backend original = new NoopBackend();
 
-        Backend result = TuiBackendHelper.applyRecording(original);
-
-        assertThat(result).isSameAs(original);
+        try (TuiRunner runner = TuiBackendHelper.createTuiRunner(original)) {
+            assertThat(runner.backend()).isSameAs(original);
+        }
     }
 
     @Test
@@ -82,13 +83,14 @@ class TuiBackendHelperRecordingTest {
         System.setProperty("tamboui.record.width", "120");
         System.setProperty("tamboui.record.height", "30");
 
-        Backend result = TuiBackendHelper.applyRecording(new NoopBackend());
-
-        // A recording backend reports the configured cast dimensions rather 
than the real terminal
-        // size; asserting on those proves the configuration was applied, not 
merely that some
-        // wrapper was returned.
-        assertThat(result).isNotInstanceOf(NoopBackend.class);
-        assertThat(result.size()).isEqualTo(new Size(120, 30));
+        try (TuiRunner runner = TuiBackendHelper.createTuiRunner(new 
NoopBackend())) {
+            // A recording backend reports the configured cast dimensions 
rather than the real terminal
+            // size; asserting on those proves the configuration was applied, 
not merely that some
+            // wrapper was returned.
+            Backend result = runner.backend();
+            assertThat(result).isNotInstanceOf(NoopBackend.class);
+            assertThat(result.size()).isEqualTo(new Size(120, 30));
+        }
     }
 
     /**
@@ -161,6 +163,11 @@ class TuiBackendHelperRecordingTest {
             return -2;
         }
 
+        @Override
+        public void writeRaw(byte[] data) throws IOException {
+            // TuiRunner.create enables bracketed paste through this method
+        }
+
         @Override
         public void close() throws IOException {
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
index 6ed452a76918..60613d46491a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
@@ -16,17 +16,19 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.util.List;
+
 import dev.tamboui.text.CharWidth;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * Validates {@link TuiIcons} primary tab emoji width (CAMEL-23818: avoid VS16 
mismeasurement in TamboUI) and the
- * mnemonic and runtime/platform icon helpers. More-submenu icons and labels 
are validated in {@link TabRegistryTest}
- * where the {@link TabRegistry.MoreTab} records that own them are constructed.
+ * Validates {@link TuiIcons} emoji widths (CAMEL-23818: every icon has to 
take the 2 columns TamboUI reserves for it,
+ * which for text-default glyphs means carrying VS16) and the mnemonic and 
runtime/platform icon helpers. More-submenu
+ * icons and labels are validated in {@link TabRegistryTest} where the {@link 
TabRegistry.MoreTab} records that own them
+ * are constructed.
  */
 class TuiIconsTest {
 
@@ -59,9 +61,12 @@ class TuiIconsTest {
     }
 
     @Test
-    void primaryTabEmojisHaveNoVariationSelector() {
-        for (String icon : TuiIcons.PRIMARY_TAB_ICONS) {
-            assertFalse(icon.contains("\uFE0F"), "Icon should not contain VS16 
variation selector: " + icon);
+    void textDefaultMenuEmojisCarryVs16AndAreTwoColumnsWide() {
+        // bare U+2328, U+23F9, U+23FA and U+1F5D1 render in one column on 
terminals; the VS16 sequence is what
+        // makes the terminal, the --web xterm.js tables and TamboUI agree on 
two
+        for (String icon : List.of(TuiIcons.KEYSTROKES, TuiIcons.RECORD, 
TuiIcons.STOP_RECORD, TuiIcons.DELETE)) {
+            assertTrue(icon.endsWith("\uFE0F"), "Icon should end with the VS16 
variation selector: " + icon);
+            assertEquals(2, CharWidth.of(icon), "Icon should be 2 terminal 
columns wide: " + icon);
         }
     }
 

Reply via email to