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/07/07 19:29:19 UTC
(camel) 01/02: chore: Reorganize TUI F2 actions menu with sub-menus
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 <cl...@gmail.com>
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 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../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);