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 9f2b1f71d92c CAMEL-23831: Add folder browser dialog and persist last 
folder in camel-tui CAMEL-23831: Fix chart axis labels to use round base-10 
numbers in camel-tui CAMEL-23831: Reduce UI freeze when switching integrations 
in camel-tui - Change default refresh interval from 100ms to 500ms - On 
Overview tab refresh all integration PIDs every cycle (not just selected) - 
Remove eager data loading from onIntegrationChanged in 6 tabs - Remove eager 
diagram preloading on integration s [...]
9f2b1f71d92c is described below

commit 9f2b1f71d92c0b126b6d8361190d45ec548fea35
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 3 12:53:41 2026 +0200

    CAMEL-23831: Add folder browser dialog and persist last folder in camel-tui
    CAMEL-23831: Fix chart axis labels to use round base-10 numbers in camel-tui
    CAMEL-23831: Reduce UI freeze when switching integrations in camel-tui
    - Change default refresh interval from 100ms to 500ms
    - On Overview tab refresh all integration PIDs every cycle (not just 
selected)
    - Remove eager data loading from onIntegrationChanged in 6 tabs
    - Remove eager diagram preloading on integration switch
    CAMEL-23831: Fix History tab scroll and add Home/End navigation in camel-tui
    CAMEL-23831: Restore scroll position when navigating back in folder browser 
in camel-tui
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  72 ++++-
 .../dsl/jbang/core/commands/tui/BeansTab.java      |   1 -
 .../dsl/jbang/core/commands/tui/BrowseTab.java     |   1 -
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   5 +-
 .../dsl/jbang/core/commands/tui/ClasspathTab.java  |   1 -
 .../core/commands/tui/DataRefreshService.java      |   5 +-
 .../dsl/jbang/core/commands/tui/FilesBrowser.java  |  24 +-
 .../dsl/jbang/core/commands/tui/FolderBrowser.java | 312 +++++++++++++++++++++
 .../jbang/core/commands/tui/HeapHistogramTab.java  |   1 -
 .../dsl/jbang/core/commands/tui/HistoryTab.java    |  41 ++-
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  48 +++-
 .../dsl/jbang/core/commands/tui/StartupTab.java    |   1 -
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |   4 -
 .../dsl/jbang/core/commands/tui/ThreadsTab.java    |   1 -
 14 files changed, 475 insertions(+), 42 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 7e5d53dafa20..a9ce44650612 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
@@ -60,9 +60,11 @@ import dev.tamboui.widgets.list.ScrollMode;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
 import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -158,11 +160,13 @@ class ActionsPopup {
     private int infraImplIndex;
     private TextInputState infraPortState;
 
+    private static final String PROP_LAST_FOLDER = "camel.tui.lastFolder";
     private boolean showFolderInput;
     private TextInputState folderInputState;
     private final List<String> folderHistory = new ArrayList<>();
     private int folderHistoryIndex = -1;
     private String selectedFolder;
+    private final FolderBrowser folderBrowser = new FolderBrowser();
 
     private final McpLogPopup mcpLogPopup = new McpLogPopup();
     private final AiLogPopup aiLogPopup = new AiLogPopup();
@@ -327,7 +331,8 @@ class ActionsPopup {
     }
 
     boolean isVisible() {
-        return showActionsMenu || showGotoPopup || showExampleBrowser || 
showFolderInput || runOptionsForm.isVisible()
+        return showActionsMenu || showGotoPopup || showExampleBrowser || 
showFolderInput || folderBrowser.isVisible()
+                || runOptionsForm.isVisible()
                 || showDocPicker || showDocViewer
                 || showInfraBrowser || showInfraPortDialog
                 || mcpLogPopup.isVisible() || aiLogPopup.isVisible() || 
doctorPopup.isVisible()
@@ -378,8 +383,8 @@ class ActionsPopup {
         labels.add("───");
         // Group 1: User Actions
         labels.add("Send Message");
-        labels.add("Run an example...");
-        labels.add("Run from folder...");
+        labels.add("Run an Example...");
+        labels.add("Run from Folder...");
         labels.add("Run Dev/Infra Service...");
         labels.add("Browse Files...");
         labels.add("Stop All");
@@ -550,12 +555,23 @@ class ActionsPopup {
             }
             return true;
         }
+        if (folderBrowser.isVisible()) {
+            folderBrowser.handleKeyEvent(ke);
+            if (!folderBrowser.isVisible() && !showFolderInput) {
+                showFolderInput = true;
+            }
+            return true;
+        }
         if (showFolderInput) {
             if (ke.isCancel()) {
                 showFolderInput = false;
                 showActionsMenu = true;
             } else if (ke.isConfirm()) {
                 confirmFolderInput();
+            } else if (ke.isKey(KeyCode.TAB)) {
+                String current = folderInputState.text().trim();
+                showFolderInput = false;
+                folderBrowser.open(current);
             } else if (ke.isUp()) {
                 navigateFolderHistory(-1);
             } else if (ke.isDown()) {
@@ -805,6 +821,9 @@ class ActionsPopup {
         if (showInfraPortDialog) {
             renderInfraPortDialog(frame, area);
         }
+        if (folderBrowser.isVisible()) {
+            folderBrowser.render(frame, area);
+        }
         if (showFolderInput) {
             renderFolderInput(frame, area);
         }
@@ -880,10 +899,15 @@ class ActionsPopup {
             runOptionsForm.renderFooter(spans);
             return;
         }
+        if (folderBrowser.isVisible()) {
+            folderBrowser.renderFooter(spans);
+            return;
+        }
         if (showFolderInput) {
             if (!folderHistory.isEmpty()) {
                 hint(spans, "↑↓", "history");
             }
+            hint(spans, "Tab", "browse");
             hint(spans, "Enter", "run...");
             hintLast(spans, "Esc", "back");
             return;
@@ -963,8 +987,8 @@ class ActionsPopup {
         items.add(hasSelection
                 ? ListItem.from("  📩 Send Message")
                 : ListItem.from("  📩 Send Message").style(Style.EMPTY.dim()));
-        items.add(ListItem.from("  🐪 Run an example..."));
-        items.add(ListItem.from("  📂 Run from folder..."));
+        items.add(ListItem.from("  🐪 Run an Example..."));
+        items.add(ListItem.from("  📂 Run from Folder..."));
         items.add(ListItem.from("  🔧 Run Dev/Infra Service..."));
         items.add(hasSelection
                 ? ListItem.from("  📁 Browse Files...")
@@ -1437,8 +1461,16 @@ class ActionsPopup {
     private void openFolderInput() {
         showActionsMenu = false;
         showFolderInput = true;
-        folderInputState = new TextInputState("");
+        String lastFolder = loadLastFolder();
+        if (lastFolder == null) {
+            lastFolder = System.getProperty("user.dir");
+        }
+        folderInputState = new TextInputState(lastFolder != null ? lastFolder 
: "");
         folderHistoryIndex = -1;
+        folderBrowser.setOnSelect(path -> {
+            folderInputState = new TextInputState(path);
+            showFolderInput = true;
+        });
     }
 
     private void confirmFolderInput() {
@@ -1462,10 +1494,36 @@ class ActionsPopup {
         }
         selectedFolder = folder;
         showFolderInput = false;
+        persistLastFolder(folder);
         String displayName = dirPath.getFileName().toString();
         runOptionsForm.open(displayName, displayName, false, true);
     }
 
+    private static String loadLastFolder() {
+        String[] holder = { null };
+        try {
+            CommandLineHelper.loadProperties(props -> {
+                holder[0] = props.getProperty(PROP_LAST_FOLDER);
+            });
+        } catch (RuntimeException e) {
+            // ignore
+        }
+        return holder[0];
+    }
+
+    private static void persistLastFolder(String folder) {
+        try {
+            CommandLineHelper.createPropertyFile(false);
+            CommandLineHelper.loadProperties(props -> {
+                props.setProperty(PROP_LAST_FOLDER, folder);
+                CommandLineHelper.storeProperties(props,
+                        new Printer.QuietPrinter(new 
Printer.SystemOutPrinter()), false);
+            });
+        } catch (Exception e) {
+            // ignore
+        }
+    }
+
     private void navigateFolderHistory(int direction) {
         if (folderHistory.isEmpty()) {
             return;
@@ -1520,7 +1578,7 @@ class ActionsPopup {
 
         Block block = Block.builder()
                 .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                .title(" Run from folder ")
+                .title(" Run from Folder ")
                 .build();
         frame.renderWidget(block, popup);
         Rect inner = block.inner(popup);
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 cad5f1964ee3..072850109e1a 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
@@ -84,7 +84,6 @@ class BeansTab extends AbstractTableTab {
         showDetail = false;
         detailScroll = 0;
         lastPid = null;
-        loadBeans();
     }
 
     @Override
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 aa1780fd4f47..480f707f5e9d 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
@@ -110,7 +110,6 @@ class BrowseTab extends AbstractTab {
         view = VIEW_ENDPOINTS;
         detailScroll = 0;
         lastPid = null;
-        loadEndpoints();
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index fa6ec335c4ea..6114ad04fbfd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -75,7 +75,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
 public class CamelMonitor extends CamelCommand {
 
     private static final Logger LOG = 
System.getLogger(CamelMonitor.class.getName());
-    private static final long DEFAULT_REFRESH_MS = 100;
+    private static final long DEFAULT_REFRESH_MS = 500;
 
     // Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is 
the true minimum
     private static final int MIN_WIDTH = 88;
@@ -1789,9 +1789,6 @@ public class CamelMonitor extends CamelCommand {
         if (ctx.selectedPid != null && !isInfraSelected()) {
             IntegrationInfo selInfo = findSelectedIntegration();
             if (selInfo != null) {
-                if (selInfo.directory != null && !selInfo.directory.isEmpty()) 
{
-                    hint(spans, "f", "files");
-                }
                 hint(spans, "p", selInfo.routeStarted > 0 ? "stop routes" : 
"start routes");
             }
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index df85defa5013..f418f2aca1b1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -90,7 +90,6 @@ class ClasspathTab extends AbstractTab {
         lastPid = null;
         errorMessage = null;
         dataLoaded = false;
-        loadClasspath();
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index 4d449c991268..7285ed4337fc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -240,7 +240,8 @@ class DataRefreshService {
         }
 
         List<Long> refreshPids;
-        if (!fullScan && ctx.selectedPid != null) {
+        boolean overviewTab = refreshCtx.selectedTab() == 
TabRegistry.TAB_OVERVIEW;
+        if (!fullScan && ctx.selectedPid != null && !overviewTab) {
             try {
                 refreshPids = List.of(Long.parseLong(ctx.selectedPid));
             } catch (NumberFormatException e) {
@@ -267,7 +268,7 @@ class DataRefreshService {
                 }
             }
         }
-        if (!fullScan && ctx.selectedPid != null) {
+        if (!fullScan && ctx.selectedPid != null && !overviewTab) {
             boolean checkLiveness = now - lastLivenessCheckTime >= 
LIVENESS_CHECK_INTERVAL_MS;
             if (checkLiveness) {
                 lastLivenessCheckTime = now;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
index 26f1e5309ab9..47ecfa8d48d7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
@@ -113,7 +113,7 @@ class FilesBrowser {
             stream.limit(200)
                     .forEach(p -> {
                         String name = p.getFileName().toString();
-                        if (Files.isDirectory(p)) {
+                        if (Files.isDirectory(p) && !name.startsWith(".")) {
                             dirs.add(new FileEntry("📁", name, -1, 
p.toString(), true));
                         } else if (Files.isRegularFile(p)) {
                             String emoji = fileEmoji(p);
@@ -171,6 +171,26 @@ class FilesBrowser {
                 listState.selectNext(entries.size());
                 return true;
             }
+            if (ke.isPageUp()) {
+                for (int i = 0; i < 20; i++) {
+                    listState.selectPrevious();
+                }
+                return true;
+            }
+            if (ke.isPageDown()) {
+                for (int i = 0; i < 20; i++) {
+                    listState.selectNext(entries.size());
+                }
+                return true;
+            }
+            if (ke.isHome()) {
+                listState.select(0);
+                return true;
+            }
+            if (ke.isEnd()) {
+                listState.select(entries.size() - 1);
+                return true;
+            }
             if (ke.isDeleteBackward()) {
                 if (currentDir != null && !currentDir.equals(rootDir)) {
                     loadDirectory(currentDir.getParent());
@@ -249,7 +269,7 @@ class FilesBrowser {
                 .items(items)
                 .highlightStyle(Theme.selectionBg())
                 .highlightSymbol("")
-                .scrollMode(ScrollMode.NONE)
+                .scrollMode(ScrollMode.AUTO_SCROLL)
                 .block(Block.builder()
                         .borderType(BorderType.ROUNDED).borders(Borders.ALL)
                         .title(Title.from(Line
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
new file mode 100644
index 000000000000..8322f48b5851
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
@@ -0,0 +1,312 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.List;
+import java.util.function.Consumer;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+
+class FolderBrowser {
+
+    record DirEntry(String name, String path) {
+    }
+
+    private boolean visible;
+    private Path currentDir;
+    private final ListState listState = new ListState();
+    private final Deque<Integer> offsetStack = new ArrayDeque<>();
+    private List<DirEntry> entries = Collections.emptyList();
+    private Consumer<String> onSelect;
+    private char lastJumpChar;
+    private int lastJumpIndex = -1;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void setOnSelect(Consumer<String> onSelect) {
+        this.onSelect = onSelect;
+    }
+
+    void open(String startPath) {
+        Path start = null;
+        if (startPath != null && !startPath.isEmpty()) {
+            String resolved = startPath;
+            if (resolved.startsWith("~")) {
+                resolved = System.getProperty("user.home") + 
resolved.substring(1);
+            }
+            Path p = Path.of(resolved);
+            if (Files.isDirectory(p)) {
+                start = p;
+            }
+        }
+        if (start == null) {
+            start = Path.of(System.getProperty("user.dir"));
+        }
+        if (loadDirectory(start)) {
+            visible = true;
+            offsetStack.clear();
+        }
+    }
+
+    private boolean loadDirectory(Path dir) {
+        return loadDirectory(dir, null);
+    }
+
+    private boolean loadDirectory(Path dir, String selectName) {
+        List<DirEntry> dirs = new ArrayList<>();
+        try (var stream = Files.list(dir)) {
+            stream.filter(Files::isDirectory)
+                    .filter(p -> !p.getFileName().toString().startsWith("."))
+                    .limit(200)
+                    .forEach(p -> dirs.add(new 
DirEntry(p.getFileName().toString(), p.toString())));
+        } catch (IOException e) {
+            return false;
+        }
+        dirs.sort(Comparator.comparing(DirEntry::name, 
String.CASE_INSENSITIVE_ORDER));
+
+        List<DirEntry> found = new ArrayList<>();
+        Path parent = dir.getParent();
+        if (parent != null) {
+            found.add(new DirEntry("..", parent.toString()));
+        }
+        found.addAll(dirs);
+
+        if (found.isEmpty()) {
+            return false;
+        }
+        entries = found;
+        int sel = 0;
+        if (selectName != null) {
+            for (int i = 0; i < found.size(); i++) {
+                if (found.get(i).name().equals(selectName)) {
+                    sel = i;
+                    break;
+                }
+            }
+        }
+        listState.select(sel);
+        currentDir = dir;
+        lastJumpChar = 0;
+        lastJumpIndex = -1;
+        return true;
+    }
+
+    private void navigateBack() {
+        String childName = currentDir.getFileName().toString();
+        if (loadDirectory(currentDir.getParent(), childName)) {
+            int savedOffset = offsetStack.isEmpty() ? 0 : offsetStack.pop();
+            listState.setOffset(savedOffset);
+        }
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isCancel()) {
+            visible = false;
+            return true;
+        }
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            lastJumpChar = 0;
+            lastJumpIndex = -1;
+            return true;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(entries.size());
+            lastJumpChar = 0;
+            lastJumpIndex = -1;
+            return true;
+        }
+        if (ke.isPageUp()) {
+            for (int i = 0; i < 20; i++) {
+                listState.selectPrevious();
+            }
+            lastJumpChar = 0;
+            lastJumpIndex = -1;
+            return true;
+        }
+        if (ke.isPageDown()) {
+            for (int i = 0; i < 20; i++) {
+                listState.selectNext(entries.size());
+            }
+            lastJumpChar = 0;
+            lastJumpIndex = -1;
+            return true;
+        }
+        if (ke.isHome()) {
+            listState.select(0);
+            lastJumpChar = 0;
+            lastJumpIndex = -1;
+            return true;
+        }
+        if (ke.isEnd()) {
+            listState.select(entries.size() - 1);
+            lastJumpChar = 0;
+            lastJumpIndex = -1;
+            return true;
+        }
+        if (ke.isDeleteBackward()) {
+            if (currentDir != null && currentDir.getParent() != null) {
+                navigateBack();
+            }
+            return true;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = listState.selected();
+            if (sel != null && sel < entries.size()) {
+                DirEntry entry = entries.get(sel);
+                if ("..".equals(entry.name()) && currentDir != null) {
+                    navigateBack();
+                } else {
+                    offsetStack.push(listState.offset());
+                    loadDirectory(Path.of(entry.path()));
+                }
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.TAB)) {
+            visible = false;
+            if (onSelect != null && currentDir != null) {
+                onSelect.accept(currentDir.toString());
+            }
+            return true;
+        }
+        if (ke.code() == KeyCode.CHAR) {
+            boolean reverse = Character.isUpperCase(ke.string().charAt(0));
+            char c = Character.toLowerCase(ke.string().charAt(0));
+            int found = -1;
+            if (reverse) {
+                int startFrom = c == lastJumpChar && lastJumpIndex >= 0 ? 
lastJumpIndex - 1 : entries.size() - 1;
+                for (int i = startFrom; i >= 0; i--) {
+                    String name = entries.get(i).name();
+                    if (!name.isEmpty() && 
Character.toLowerCase(name.charAt(0)) == c) {
+                        found = i;
+                        break;
+                    }
+                }
+                if (found < 0) {
+                    for (int i = entries.size() - 1; i > startFrom; i--) {
+                        String name = entries.get(i).name();
+                        if (!name.isEmpty() && 
Character.toLowerCase(name.charAt(0)) == c) {
+                            found = i;
+                            break;
+                        }
+                    }
+                }
+            } else {
+                int startFrom = c == lastJumpChar && lastJumpIndex >= 0 ? 
lastJumpIndex + 1 : 0;
+                for (int i = startFrom; i < entries.size(); i++) {
+                    String name = entries.get(i).name();
+                    if (!name.isEmpty() && 
Character.toLowerCase(name.charAt(0)) == c) {
+                        found = i;
+                        break;
+                    }
+                }
+                if (found < 0) {
+                    for (int i = 0; i < startFrom && i < entries.size(); i++) {
+                        String name = entries.get(i).name();
+                        if (!name.isEmpty() && 
Character.toLowerCase(name.charAt(0)) == c) {
+                            found = i;
+                            break;
+                        }
+                    }
+                }
+            }
+            if (found >= 0) {
+                listState.select(found);
+                lastJumpChar = c;
+                lastJumpIndex = found;
+            }
+            return true;
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (entries.isEmpty()) {
+            visible = false;
+            return;
+        }
+
+        String dirLabel = currentDir != null ? currentDir.toString() : "";
+        String popupTitle = " 📂 " + dirLabel + " ";
+
+        int nameWidth = entries.stream().mapToInt(e -> 
e.name().length()).max().orElse(10);
+        int itemWidth = 6 + nameWidth;
+        int titleWidth = popupTitle.length() + 4;
+        int popupW = Math.min(area.width() - 4, Math.max(50, 
Math.max(itemWidth + 4, titleWidth)));
+        int popupH = Math.min(area.height() - 4, Math.max(12, entries.size() + 
2));
+
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        ListItem[] items = new ListItem[entries.size()];
+        for (int i = 0; i < entries.size(); i++) {
+            DirEntry entry = entries.get(i);
+            String label = "  📁 " + entry.name();
+            items[i] = ListItem.from(Line.from(Span.styled(label, 
Style.EMPTY.fg(Color.CYAN))));
+        }
+
+        ListWidget list = ListWidget.builder()
+                .items(items)
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(Title.from(Line
+                                .from(Span.styled(popupTitle, 
Style.EMPTY.fg(Color.YELLOW).bold()))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, listState);
+    }
+
+    void renderFooter(List<Span> spans) {
+        TuiHelper.hint(spans, "↑↓", "navigate");
+        TuiHelper.hint(spans, "Enter", "open");
+        TuiHelper.hint(spans, "Tab", "select");
+        TuiHelper.hintLast(spans, "Esc", "close");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
index 5a2004fd154b..f6b713975777 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -88,7 +88,6 @@ class HeapHistogramTab extends AbstractTableTab {
         allEntries = Collections.emptyList();
         classpathEntries = Collections.emptyList();
         lastPid = null;
-        loadHeapHistogram();
     }
 
     @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 f9c960c90344..883325c325b7 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
@@ -129,6 +129,7 @@ class HistoryTab extends AbstractTab {
     private final DragSplit detailSplit = new DragSplit();
 
     volatile List<HistoryEntry> historyEntries = Collections.emptyList();
+    private volatile int historyVisibleCount;
     private final TableState historyTableState = new TableState();
     private boolean showHistoryProperties;
     private boolean showHistoryVariables;
@@ -297,7 +298,7 @@ class HistoryTab extends AbstractTab {
             } else {
                 if (showWaterfall) {
                     for (int i = 0; i < 10; i++) {
-                        historyTableState.selectNext(historyEntries.size());
+                        historyTableState.selectNext(historyVisibleCount);
                     }
                 } else {
                     historyDetailScroll += 5;
@@ -305,6 +306,31 @@ class HistoryTab extends AbstractTab {
             }
             return true;
         }
+        if (ke.isHome()) {
+            if (tracerActive && traceDetailView) {
+                traceStepTableState.selectFirst();
+                traceDetailScroll = 0;
+            } else if (tracerActive) {
+                traceTableState.selectFirst();
+            } else {
+                historyTableState.selectFirst();
+                historyDetailScroll = 0;
+            }
+            return true;
+        }
+        if (ke.isEnd()) {
+            if (tracerActive && traceDetailView) {
+                List<TraceEntry> steps = 
getTraceStepsDepthFirst(traceSelectedExchangeId);
+                traceStepTableState.selectLast(steps.size());
+                traceDetailScroll = 0;
+            } else if (tracerActive) {
+                traceTableState.selectLast(traceSortedExchangeIds.size());
+            } else {
+                historyTableState.selectLast(historyVisibleCount);
+                historyDetailScroll = 0;
+            }
+            return true;
+        }
         if (ke.isLeft()) {
             if (tracerActive && traceDetailView && !traceWordWrap) {
                 traceDetailHScroll = Math.max(0, traceDetailHScroll - 4);
@@ -520,7 +546,7 @@ class HistoryTab extends AbstractTab {
             }
         }
         if (!tracerActive) {
-            if (handleTableClick(me, lastHistoryTableArea, historyTableState, 
historyEntries.size())) {
+            if (handleTableClick(me, lastHistoryTableArea, historyTableState, 
historyVisibleCount)) {
                 historyDetailScroll = 0;
                 return true;
             }
@@ -570,7 +596,7 @@ class HistoryTab extends AbstractTab {
                 }
             } else {
                 if (isInArea(me, lastHistoryTableArea) || showWaterfall) {
-                    historyTableState.selectNext(historyEntries.size());
+                    historyTableState.selectNext(historyVisibleCount);
                     historyDetailScroll = 0;
                 } else {
                     historyDetailScroll += MOUSE_SCROLL_LINES;
@@ -612,7 +638,7 @@ class HistoryTab extends AbstractTab {
                 traceTableState.selectNext(exchangeIds.size());
             }
         } else {
-            historyTableState.selectNext(historyEntries.size());
+            historyTableState.selectNext(historyVisibleCount);
             historyDetailScroll = 0;
         }
     }
@@ -1210,6 +1236,8 @@ class HistoryTab extends AbstractTab {
                 .build();
 
         lastTraceTableArea = area;
+        int traceVisibleRows = Math.max(0, area.height() - 3);
+        traceTableState.scrollToSelected(traceVisibleRows, rows);
         frame.renderStatefulWidget(table, area, traceTableState);
         renderTableScrollbar(frame, lastTraceTableArea, traceTableState, 
tableScrollState,
                 traceSortedExchangeIds.size());
@@ -1239,6 +1267,8 @@ class HistoryTab extends AbstractTab {
                 = String.format(" Trace [%s] — %d steps ", 
TuiHelper.truncate(traceSelectedExchangeId, 30), steps.size());
         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), 
chunks.get(0), traceStepTableState);
         renderTableScrollbar(frame, lastTraceStepArea, traceStepTableState, 
traceStepScrollState,
@@ -1493,6 +1523,7 @@ class HistoryTab extends AbstractTab {
         }
 
         List<HistoryEntry> current = reorderHistoryDepthFirst(historyEntries);
+        historyVisibleCount = current.size();
 
         historyTopPanelHeight = Math.max(3, Math.min(historyTopPanelHeight, 
area.height() - 5));
         List<Rect> chunks = Layout.vertical()
@@ -1514,6 +1545,8 @@ class HistoryTab extends AbstractTab {
         Title historyTitle = buildHistoryTitle(current);
         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), 
chunks.get(0), historyTableState);
         renderTableScrollbar(frame, lastHistoryTableArea, historyTableState, 
historyTableScrollState,
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 e6d8691d09d1..a33d0944f632 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
@@ -488,10 +488,11 @@ class OverviewTab extends AbstractTab {
                 }
             }
 
-            long maxTp = 0;
+            long rawMax = 0;
             for (long v : mergedTotal) {
-                maxTp = Math.max(maxTp, v);
+                rawMax = Math.max(rawMax, v);
             }
+            long maxTp = roundUpNice(rawMax);
             long curTp = mergedTotal[renderPoints - 1];
             long curFailed = mergedFailed[renderPoints - 1];
             long curOk = Math.max(0, curTp - curFailed);
@@ -561,17 +562,26 @@ class OverviewTab extends AbstractTab {
             if (!vChunks.get(1).isEmpty()) {
                 int barInnerStartX = barChartArea.x();
                 int xAxisY = vChunks.get(1).y();
-                int[][] markerIndices = {
-                        { 0, renderPoints },
-                        { renderPoints / 4, renderPoints - renderPoints / 4 },
-                        { renderPoints / 2, renderPoints / 2 },
-                        { 3 * renderPoints / 4, renderPoints / 4 },
-                        { renderPoints - 1, 0 }
-                };
-                for (int[] m : markerIndices) {
-                    int groupIdx = m[0];
-                    int secsAgo = m[1];
-                    String label = secsAgo == 0 ? "now" : "-" + secsAgo + "s";
+                int step;
+                if (renderPoints <= 20) {
+                    step = 5;
+                } else if (renderPoints <= 80) {
+                    step = 10;
+                } else {
+                    step = 20;
+                }
+                // "now" label at the right edge
+                int nowX = barInnerStartX + (renderPoints - 1) * 2;
+                if (nowX + 3 <= barChartArea.right()) {
+                    frame.buffer().setString(nowX, xAxisY, "now", dimStyle);
+                }
+                // round time markers from right to left
+                for (int s = step; s <= renderPoints; s += step) {
+                    int groupIdx = renderPoints - 1 - s;
+                    if (groupIdx < 0) {
+                        break;
+                    }
+                    String label = "-" + s + "s";
                     int markerX = barInnerStartX + groupIdx * 2;
                     if (markerX + label.length() <= barChartArea.right()) {
                         frame.buffer().setString(markerX, xAxisY, label, 
dimStyle);
@@ -882,6 +892,18 @@ class OverviewTab extends AbstractTab {
         return sortStyle(column, sort);
     }
 
+    private static long roundUpNice(long value) {
+        if (value <= 10) {
+            return 10;
+        }
+        long step = (long) Math.pow(10, Math.floor(Math.log10(value)));
+        long rounded = ((value + step - 1) / step) * step;
+        if (rounded % 2 != 0) {
+            rounded += step;
+        }
+        return rounded;
+    }
+
     private static boolean hasReadmeInSourceDir(IntegrationInfo info) {
         java.nio.file.Path srcDir = FilesBrowser.resolveSourceDirectory(info);
         if (srcDir != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
index 386b080e6431..3f097462f4f1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -139,7 +139,6 @@ class StartupTab extends AbstractTab {
         scrollOffset = 0;
         errorMessage = null;
         dataLoaded = false;
-        loadStartupData();
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index b5efb3570005..d8fd97204900 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -265,10 +265,6 @@ class TabRegistry {
         dataService.otelSpans().set(List.of());
 
         filesBrowser.reset();
-
-        // Preload diagram data in background so it's ready when the user 
switches tabs
-        routesTab.preloadDiagram();
-        diagramTab.preloadDiagram();
     }
 
     void navigateUp() {
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 c79e36215ac4..04aad8b40ad8 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
@@ -88,7 +88,6 @@ class ThreadsTab extends AbstractTableTab {
         showTrace = false;
         traceScroll = 0;
         lastPid = null;
-        loadThreads();
     }
 
     @Override


Reply via email to