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

commit 2f7c3304c322f422536fb6078964bc660eb184bc
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jul 7 20:37:51 2026 +0200

    chore: Reorganize TUI F2 actions menu with sub-menus
    
    Move less frequently used items into sub-menus to keep the
    top-level actions menu focused on daily-driver operations.
    
    Screen sub-menu: screenshot, theme toggle, reset screen,
    tape recording, tape guide, caption, and show keystrokes.
    
    AI & MCP sub-menu: setup MCP, AI log, MCP info, MCP log.
    
    Sub-menus use the same folder/back navigation pattern as
    the example browser (.. to go back, Esc returns to parent).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 260 ++++++++++++++-------
 1 file changed, 175 insertions(+), 85 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 9449dc1c06ea..acd322e03f8b 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
@@ -59,10 +59,14 @@ class ActionsPopup {
         BROWSE_FILES,
         DOCTOR,
         RESET_STATS,
-        RESET_SCREEN,
-        TOGGLE_THEME,
         STOP_ALL,
+        SHELL,
+        SCREEN_SUBMENU,
+        MCP_SUBMENU,
+        BACK,
         SCREENSHOT,
+        TOGGLE_THEME,
+        RESET_SCREEN,
         TAPE_RECORDING,
         TAPE_INSTRUCTIONS,
         CAPTION,
@@ -70,14 +74,9 @@ class ActionsPopup {
         SETUP_AI,
         MCP_INFO,
         MCP_LOG,
-        AI_LOG,
-        SHELL
+        AI_LOG
     }
 
-    private static final int[] GROUP_SIZES = { 2, 6, 4, 5 };
-    private static final int MCP_GROUP_SIZE = 4;
-    private static final int SHELL_GROUP_SIZE = 1;
-
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
     private final Supplier<List<InfraInfo>> infraServices;
@@ -101,6 +100,8 @@ class ActionsPopup {
 
     private boolean showActionsMenu;
     private final ListState actionsMenuState = new ListState();
+    private String currentSubmenu;
+    private int savedMainSelection;
     // Absolute bounds of the single-line list popups captured during render, 
used to hit-test clicks.
     private Rect actionsMenuRect;
 
@@ -214,38 +215,12 @@ class ActionsPopup {
     }
 
     private int visualActionCount() {
-        int total = 0;
-        for (int gs : GROUP_SIZES) {
-            total += gs;
-        }
-        int dividers = GROUP_SIZES.length - 1;
-        if (mcpEnabled) {
-            total += MCP_GROUP_SIZE;
-            dividers++;
-        }
-        total += SHELL_GROUP_SIZE;
-        dividers++;
-        return total + dividers;
+        return buildVisualActionList().size();
     }
 
     private boolean isDividerIndex(int visualIndex) {
-        int pos = 0;
-        for (int i = 0; i < GROUP_SIZES.length; i++) {
-            pos += GROUP_SIZES[i];
-            if (visualIndex == pos) {
-                return true;
-            }
-            pos++;
-        }
-        if (mcpEnabled) {
-            pos += MCP_GROUP_SIZE;
-            if (visualIndex == pos) {
-                return true;
-            }
-            pos++;
-        }
-        pos += SHELL_GROUP_SIZE;
-        return false;
+        List<Action> flat = buildVisualActionList();
+        return visualIndex >= 0 && visualIndex < flat.size() && 
flat.get(visualIndex) == null;
     }
 
     private Action resolveAction(int visualIndex) {
@@ -257,6 +232,16 @@ class ActionsPopup {
     }
 
     private List<Action> buildVisualActionList() {
+        if ("screen".equals(currentSubmenu)) {
+            return buildScreenSubmenuList();
+        }
+        if ("mcp".equals(currentSubmenu)) {
+            return buildMcpSubmenuList();
+        }
+        return buildMainMenuList();
+    }
+
+    private List<Action> buildMainMenuList() {
         List<Action> flat = new ArrayList<>();
         flat.add(Action.GOTO_TAB);
         flat.add(Action.SWITCH_INTEGRATION);
@@ -265,20 +250,34 @@ class ActionsPopup {
                 Action.SEND_MESSAGE, Action.RUN_EXAMPLE, Action.RUN_FOLDER, 
Action.RUN_INFRA, Action.BROWSE_FILES,
                 Action.STOP_ALL));
         flat.add(null);
-        flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS, 
Action.RESET_SCREEN, Action.TOGGLE_THEME));
-        flat.add(null);
-        flat.addAll(List.of(
-                Action.SCREENSHOT, Action.TAPE_RECORDING, 
Action.TAPE_INSTRUCTIONS, Action.CAPTION,
-                Action.SHOW_KEYSTROKES));
+        flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS, 
Action.SCREEN_SUBMENU));
         if (mcpEnabled) {
             flat.add(null);
-            flat.addAll(List.of(Action.SETUP_AI, Action.AI_LOG, 
Action.MCP_INFO, Action.MCP_LOG));
+            flat.add(Action.MCP_SUBMENU);
         }
         flat.add(null);
         flat.add(Action.SHELL);
         return flat;
     }
 
+    private List<Action> buildScreenSubmenuList() {
+        List<Action> flat = new ArrayList<>();
+        flat.add(Action.BACK);
+        flat.add(null);
+        flat.addAll(List.of(Action.SCREENSHOT, Action.TOGGLE_THEME, 
Action.RESET_SCREEN));
+        flat.add(null);
+        flat.addAll(List.of(Action.TAPE_RECORDING, Action.TAPE_INSTRUCTIONS, 
Action.CAPTION, Action.SHOW_KEYSTROKES));
+        return flat;
+    }
+
+    private List<Action> buildMcpSubmenuList() {
+        List<Action> flat = new ArrayList<>();
+        flat.add(Action.BACK);
+        flat.add(null);
+        flat.addAll(List.of(Action.SETUP_AI, Action.AI_LOG, Action.MCP_INFO, 
Action.MCP_LOG));
+        return flat;
+    }
+
     private void navigateActionsMenu(int direction) {
         int total = visualActionCount();
         Integer current = actionsMenuState.selected();
@@ -350,11 +349,32 @@ class ActionsPopup {
 
     List<String> getActionLabels() {
         List<String> labels = new ArrayList<>();
-        // Group 0: Navigation
+        if ("screen".equals(currentSubmenu)) {
+            labels.add("..");
+            labels.add("───");
+            labels.add("Take Screenshot");
+            labels.add(Theme.isDark() ? "Light Theme" : "Dark Theme");
+            labels.add("Reset Screen");
+            labels.add("───");
+            labels.add(tapeRecordingActive.get() ? "Stop Tape Recording" : 
"Start Tape Recording");
+            labels.add("Tape Recording Guide");
+            labels.add("Caption...");
+            labels.add(keystrokesEnabled.get() ? "Hide Keystrokes" : "Show 
Keystrokes");
+            return labels;
+        }
+        if ("mcp".equals(currentSubmenu)) {
+            labels.add("..");
+            labels.add("───");
+            labels.add("Setup MCP");
+            labels.add("AI Log");
+            labels.add("MCP Info");
+            labels.add("MCP Log");
+            return labels;
+        }
+        // Main menu
         labels.add("Go to...");
         labels.add("Switch Integration (F3)");
         labels.add("───");
-        // Group 1: User Actions
         labels.add("Send Message");
         labels.add("Run an Example...");
         labels.add("Run from Folder...");
@@ -362,25 +382,12 @@ class ActionsPopup {
         labels.add("Browse Files...");
         labels.add("Stop All");
         labels.add("───");
-        // Group 2: Diagnostics
         labels.add("Run Doctor");
         labels.add("Reset Stats");
-        labels.add("Reset Screen");
-        labels.add(Theme.isDark() ? "Light Theme" : "Dark Theme");
-        labels.add("───");
-        // Group 3: Recording & Presentation
-        labels.add("Take Screenshot");
-        labels.add(tapeRecordingActive.get() ? "Stop Tape Recording" : "Start 
Tape Recording");
-        labels.add("Tape Recording Guide");
-        labels.add("Caption...");
-        labels.add(keystrokesEnabled.get() ? "Hide Keystrokes" : "Show 
Keystrokes");
-        // Group 4: MCP
+        labels.add("Screen...");
         if (mcpEnabled) {
             labels.add("───");
-            labels.add("Setup MCP");
-            labels.add("AI Log");
-            labels.add("MCP Info");
-            labels.add("MCP Log");
+            labels.add("AI & MCP...");
         }
         labels.add("───");
         labels.add("Shell");
@@ -389,11 +396,13 @@ class ActionsPopup {
 
     void open() {
         showActionsMenu = true;
+        currentSubmenu = null;
         actionsMenuState.select(0);
     }
 
     void close() {
         showActionsMenu = false;
+        currentSubmenu = null;
         gotoTabPopup.close();
         exampleBrowserPopup.close();
         folderInputPopup.close();
@@ -508,7 +517,12 @@ class ActionsPopup {
         }
         if (showActionsMenu) {
             if (ke.isCancel()) {
-                showActionsMenu = false;
+                if (currentSubmenu != null) {
+                    currentSubmenu = null;
+                    actionsMenuState.select(savedMainSelection);
+                } else {
+                    showActionsMenu = false;
+                }
             } else if (ke.isUp()) {
                 navigateActionsMenu(-1);
             } else if (ke.isDown()) {
@@ -534,6 +548,17 @@ class ActionsPopup {
                     Action action = resolveAction(sel);
                     if (action == null) {
                         // divider selected, ignore
+                    } else if (action == Action.BACK) {
+                        currentSubmenu = null;
+                        actionsMenuState.select(savedMainSelection);
+                    } else if (action == Action.SCREEN_SUBMENU) {
+                        savedMainSelection = sel;
+                        currentSubmenu = "screen";
+                        actionsMenuState.select(0);
+                    } else if (action == Action.MCP_SUBMENU) {
+                        savedMainSelection = sel;
+                        currentSubmenu = "mcp";
+                        actionsMenuState.select(0);
                     } else if (action == Action.GOTO_TAB) {
                         showActionsMenu = false;
                         gotoTabPopup.open();
@@ -797,7 +822,7 @@ class ActionsPopup {
         if (showActionsMenu) {
             hint(spans, TuiIcons.HINT_SCROLL, "navigate");
             hint(spans, "Enter", "select");
-            hintLast(spans, "Esc", "cancel");
+            hintLast(spans, "Esc", currentSubmenu != null ? "back" : "cancel");
         }
     }
 
@@ -808,6 +833,16 @@ class ActionsPopup {
     // ---- Rendering ----
 
     private void renderActionsMenu(Frame frame, Rect area) {
+        if ("screen".equals(currentSubmenu)) {
+            renderScreenSubmenu(frame, area);
+        } else if ("mcp".equals(currentSubmenu)) {
+            renderMcpSubmenu(frame, area);
+        } else {
+            renderMainMenu(frame, area);
+        }
+    }
+
+    private void renderMainMenu(Frame frame, Rect area) {
         int count = visualActionCount();
         int popupW = 40;
         int popupH = 2 + count;
@@ -818,17 +853,9 @@ class ActionsPopup {
 
         frame.renderWidget(Clear.INSTANCE, popup);
         String divider = "  ─────────────────────────────────";
-        // CAMEL-23818: use plain 2-wide emoji here. TamboUI mismeasures 
base-glyph + VS16
-        // sequences as 1-wide (fixed upstream in tamboui/tamboui#388), which 
left stray chars.
-        String keystrokeLabel = keystrokesEnabled.get()
-                ? TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Hide Keystrokes")
-                : TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Show Keystrokes");
         String stopLabel = stopAllPopup.hasBothGroups()
                 ? TuiIcons.menuItem(TuiIcons.STOP, "Stop All...")
                 : TuiIcons.menuItem(TuiIcons.STOP, "Stop All");
-        String tapeLabel = tapeRecordingActive.get()
-                ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording 
(Ctrl+R)")
-                : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording 
(Ctrl+R)");
 
         boolean canSwitch = hasMultipleIntegrations();
         List<ListItem> items = new ArrayList<>();
@@ -851,32 +878,95 @@ class ActionsPopup {
                 : ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER, "Browse 
Files...")).style(Style.EMPTY.dim()));
         items.add(ListItem.from(stopLabel));
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
-        // Group 2: Diagnostics
+        // Group 2: Diagnostics & Screen
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCTOR, "Run 
Doctor")));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.RESET, "Reset 
Stats")));
-        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CLEAN, "Reset 
Screen")));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.COMPUTER, 
"Screen...")));
+        // Group 3: MCP (conditional)
+        if (mcpEnabled) {
+            items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
+            items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP, "AI & 
MCP...")));
+        }
+        // Group 4: Shell
+        items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
+        items.add(ListItem.from("  >_ Shell"));
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.NONE)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Actions ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, actionsMenuState);
+    }
+
+    private void renderScreenSubmenu(Frame frame, Rect area) {
+        int count = visualActionCount();
+        int popupW = 40;
+        int popupH = 2 + count;
+        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));
+        this.actionsMenuRect = popup;
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+        String divider = "  ─────────────────────────────────";
+        String keystrokeLabel = keystrokesEnabled.get()
+                ? TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Hide Keystrokes")
+                : TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Show Keystrokes");
+        String tapeLabel = tapeRecordingActive.get()
+                ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording 
(Ctrl+R)")
+                : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording 
(Ctrl+R)");
         String themeLabel = Theme.isDark()
                 ? TuiIcons.menuItem(TuiIcons.LIGHT_THEME, "Light Theme")
                 : TuiIcons.menuItem(TuiIcons.DARK_THEME, "Dark Theme");
-        items.add(ListItem.from(themeLabel));
+
+        List<ListItem> items = new ArrayList<>();
+        items.add(ListItem.from("  .."));
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
-        // Group 3: Recording & Presentation
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.SCREENSHOT, "Take 
Screenshot")));
+        items.add(ListItem.from(themeLabel));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CLEAN, "Reset 
Screen")));
+        items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
         items.add(ListItem.from(tapeLabel));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCUMENT, "Tape 
Recording Guide")));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAPTION, 
"Caption...")));
         items.add(ListItem.from(keystrokeLabel));
-        // Group 4: MCP
-        if (mcpEnabled) {
-            items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
-            items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP_BRAIN, 
"Setup MCP")));
-            items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAPTION, "AI 
Log")));
-            items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP, "MCP 
Info")));
-            items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP_LOG, "MCP 
Log")));
-        }
-        // Group 5: Shell
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.NONE)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Screen ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, actionsMenuState);
+    }
+
+    private void renderMcpSubmenu(Frame frame, Rect area) {
+        int count = visualActionCount();
+        int popupW = 40;
+        int popupH = 2 + count;
+        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));
+        this.actionsMenuRect = popup;
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+        String divider = "  ─────────────────────────────────";
+
+        List<ListItem> items = new ArrayList<>();
+        items.add(ListItem.from("  .."));
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
-        items.add(ListItem.from("  >_ Shell"));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP_BRAIN, "Setup 
MCP")));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAPTION, "AI 
Log")));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP, "MCP Info")));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP_LOG, "MCP 
Log")));
         ListWidget list = ListWidget.builder()
                 .items(items.toArray(ListItem[]::new))
                 .highlightStyle(Theme.selectionBg())
@@ -884,7 +974,7 @@ class ActionsPopup {
                 .scrollMode(ScrollMode.NONE)
                 .block(Block.builder()
                         .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .title(" Actions ")
+                        .title(" AI & MCP ")
                         .build())
                 .build();
         frame.renderStatefulWidget(list, popup, actionsMenuState);

Reply via email to