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);
