You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/19 08:32:04 UTC

(camel) branch feature/CAMEL-24759-tamboui-05-cleanup created (now d5fd95871656)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a change to branch feature/CAMEL-24759-tamboui-05-cleanup
in repository https://gitbox.apache.org/repos/asf/camel.git


      at d5fd95871656 CAMEL-24759: camel-jbang - TUI: web terminal measures VS16 emoji like TamboUI, restoring the CAMEL-23818 icons

This branch includes the following new commits:

     new 1bc4b8e6fa1e CAMEL-24759: camel-jbang - TUI: drop the workarounds that TamboUI 0.5.0 makes redundant
     new d5fd95871656 CAMEL-24759: camel-jbang - TUI: web terminal measures VS16 emoji like TamboUI, restoring the CAMEL-23818 icons

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 02/02: CAMEL-24759: camel-jbang - TUI: web terminal measures VS16 emoji like TamboUI, restoring the CAMEL-23818 icons

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-24759-tamboui-05-cleanup
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d5fd95871656793ae0c53f9edd3fba19f3ea80be
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sat Sep 19 10:30:04 2026 +0200

    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) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java    |  2 +-
 .../camel/dsl/jbang/core/commands/tui/TuiIcons.java  | 17 ++++++++---------
 .../src/main/resources/tui/web/index.html            | 11 +++++++----
 .../main/resources/tui/web/vendor/LICENSE-xterm.txt  |  8 +++++---
 .../tui/web/vendor/xterm-addon-unicode-graphemes.js  |  2 ++
 .../tui/web/vendor/xterm-addon-unicode11.js          |  2 --
 .../dsl/jbang/core/commands/tui/TabRegistryTest.java |  3 +--
 .../dsl/jbang/core/commands/tui/TuiIconsTest.java    | 20 ++++++++++++--------
 8 files changed, 36 insertions(+), 29 deletions(-)

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/TuiIcons.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index c8e6b7bd725a..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,10 +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 (CAMEL-23818). TamboUI 0.5.0 measures a
- * base glyph + VS16 sequence as 2 columns (tamboui/tamboui#388), but the xterm.js Unicode 11 width tables that the
- * {@code --web} frontend uses treat VS16 as zero-width, so such a sequence still misaligns there. 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 {
 
@@ -62,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 = "πŸ“‘";
-    // the wastebasket (πŸ—‘) is a text-default glyph: TamboUI counts it as 2 columns while terminals draw it in 1 unless
-    // it carries VS16, which the --web frontend cannot measure (see the class javadoc), so use the cross-mark instead
-    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/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/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/TuiIconsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
index 0fa31d88d4bd..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,18 +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: no VS16 sequences, which the {@code --web} xterm.js
- * width tables measure differently from 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 {
 
@@ -60,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);
         }
     }
 


(camel) 01/02: CAMEL-24759: camel-jbang - TUI: drop the workarounds that TamboUI 0.5.0 makes redundant

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-24759-tamboui-05-cleanup
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 1bc4b8e6fa1eb4699b7894baea0a5f19b28dd47d
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sat Sep 19 10:23:37 2026 +0200

    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) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../dsl/jbang/core/commands/tui/FlowHelper.java    | 30 ++++++++++--
 .../jbang/core/commands/tui/TuiBackendHelper.java  | 26 ++--------
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |  8 +--
 .../jbang/core/commands/tui/FlowHelperTest.java    | 57 ++++++++++++++++++++++
 .../tui/TuiBackendHelperRecordingTest.java         | 41 +++++++++-------
 .../dsl/jbang/core/commands/tui/TuiIconsTest.java  |  7 +--
 6 files changed, 120 insertions(+), 49 deletions(-)

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/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..c8e6b7bd725a 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,7 +21,9 @@ 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
+ * Tab/menu icons use plain 2-column-wide emoji without VS16 variation selectors (CAMEL-23818). TamboUI 0.5.0 measures a
+ * base glyph + VS16 sequence as 2 columns (tamboui/tamboui#388), but the xterm.js Unicode 11 width tables that the
+ * {@code --web} frontend uses treat VS16 as zero-width, so such a sequence still misaligns there. Doctor and legacy
  * status glyphs may still use mixed-width symbols until migrated.
  */
 final class TuiIcons {
@@ -60,8 +62,8 @@ 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.
+    // the wastebasket (πŸ—‘) is a text-default glyph: TamboUI counts it as 2 columns while terminals draw it in 1 unless
+    // it carries VS16, which the --web frontend cannot measure (see the class javadoc), so use the cross-mark instead
     static final String DELETE = "❌";
 
     // ---- Actions menu ----
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/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..0fa31d88d4bd 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
@@ -24,9 +24,10 @@ 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} primary tab emoji width (CAMEL-23818: no VS16 sequences, which the {@code --web} xterm.js
+ * width tables measure differently from 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.
  */
 class TuiIconsTest {