Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,12 @@ class ActionsPopup {
// Group 4: MCP
private static final int ACTION_MCP_INFO = 13;
private static final int ACTION_MCP_LOG = 14;
// Group 5: Shell
private static final int ACTION_SHELL = 15;

private static final int[] GROUP_SIZES = { 4, 4, 5 };
private static final int MCP_GROUP_SIZE = 2;
private static final int SHELL_GROUP_SIZE = 1;

private final Supplier<Set<String>> runningNames;
private final Supplier<List<IntegrationInfo>> integrations;
Expand All @@ -97,6 +100,7 @@ class ActionsPopup {
private final Supplier<Boolean> keystrokesEnabled;
private final Runnable toggleTapeRecording;
private Runnable resetStatsAction;
private Runnable openShellAction;
private final Supplier<Boolean> tapeRecordingActive;
private MonitorContext ctx;
private boolean mcpEnabled;
Expand Down Expand Up @@ -180,6 +184,10 @@ void setResetStatsAction(Runnable resetStatsAction) {
this.resetStatsAction = resetStatsAction;
}

void setOpenShellAction(Runnable openShellAction) {
this.openShellAction = openShellAction;
}

void setMcpEnabled(
boolean enabled, int port, Supplier<String> connectedClient, Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
this.mcpEnabled = enabled;
Expand All @@ -190,13 +198,13 @@ void setMcpEnabled(
}

private int visualActionCount() {
// 4 + 4 + 5 actions = 13, plus 2 dividers = 15
int count = 13 + 2;
if (mcpEnabled) {
// 4 + 4 + 5 + 2 actions = 15, plus 3 dividers = 18
return 15 + 3;
} else {
// 4 + 4 + 5 actions = 13, plus 2 dividers = 15
return 13 + 2;
count += MCP_GROUP_SIZE + 1; // +1 divider
}
count += SHELL_GROUP_SIZE + 1; // +1 divider (shell group)
return count;
}

private boolean isDividerIndex(int visualIndex) {
Expand All @@ -210,16 +218,33 @@ private boolean isDividerIndex(int visualIndex) {
}
if (mcpEnabled) {
pos += MCP_GROUP_SIZE;
if (visualIndex == pos) {
return true;
}
pos++;
}
// Shell group divider
pos += SHELL_GROUP_SIZE;
return false;
}

private int resolveAction(int visualIndex) {
int dividers = 0;
int pos = 0;
int groupCount = mcpEnabled ? GROUP_SIZES.length + 1 : GROUP_SIZES.length;
int groupCount = GROUP_SIZES.length;
if (mcpEnabled) {
groupCount++;
}
groupCount++; // shell group
for (int i = 0; i < groupCount; i++) {
int gs = i < GROUP_SIZES.length ? GROUP_SIZES[i] : MCP_GROUP_SIZE;
int gs;
if (i < GROUP_SIZES.length) {
gs = GROUP_SIZES[i];
} else if (mcpEnabled && i == GROUP_SIZES.length) {
gs = MCP_GROUP_SIZE;
} else {
gs = SHELL_GROUP_SIZE;
}
pos += gs;
if (visualIndex < pos + dividers) {
break;
Expand Down Expand Up @@ -316,6 +341,9 @@ List<String> getActionLabels() {
labels.add("MCP Info");
labels.add("MCP Log");
}
// Group 5: Shell
labels.add("───");
labels.add("Shell");
return labels;
}

Expand Down Expand Up @@ -536,6 +564,11 @@ boolean handleKeyEvent(KeyEvent ke) {
} else if (action == ACTION_CAPTION) {
showActionsMenu = false;
captionOverlay.openInline();
} else if (action == ACTION_SHELL) {
showActionsMenu = false;
if (openShellAction != null) {
openShellAction.run();
}
}
}
}
Expand Down Expand Up @@ -703,6 +736,9 @@ private void renderActionsMenu(Frame frame, Rect area) {
items.add(ListItem.from(" 🤖 MCP Info"));
items.add(ListItem.from(" 📋 MCP Log"));
}
// Group 5: 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(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.KeyModifiers;
import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.tui.event.PasteEvent;
import dev.tamboui.tui.event.TickEvent;
import dev.tamboui.widgets.Clear;
Expand Down Expand Up @@ -240,6 +241,7 @@ public class CamelMonitor extends CamelCommand {
private final Queue<PendingKey> pendingKeys = new ConcurrentLinkedQueue<>();
private final List<KeyRecord> recentKeys = new ArrayList<>();
private final CaptionOverlay captionOverlay = new CaptionOverlay();
private final ShellPanel shellPanel = new ShellPanel();

private final ActionsPopup actionsPopup = new ActionsPopup(
() -> data.get().stream()
Expand Down Expand Up @@ -306,6 +308,7 @@ public Integer doCall() throws Exception {
ctx = new MonitorContext(data, infraData);
actionsPopup.setContext(ctx);
actionsPopup.setResetStatsAction(this::resetStats);
actionsPopup.setOpenShellAction(shellPanel::open);
logTab = new LogTab(ctx);
routesTab = new RoutesTab(ctx);
consumersTab = new ConsumersTab(ctx);
Expand Down Expand Up @@ -400,6 +403,9 @@ private boolean handleEvent(Event event, TuiRunner runner) {
captionOverlay.openInline();
return true;
}
if (shellPanel.isOpen()) {
return shellPanel.handleKeyEvent(ke);
}
if (actionsPopup.isVisible()) {
return actionsPopup.handleKeyEvent(ke);
}
Expand Down Expand Up @@ -511,6 +517,16 @@ private boolean handleEvent(Event event, TuiRunner runner) {
return true;
}

// F3 toggles shell panel
if (ke.isKey(KeyCode.F3)) {
if (shellPanel.isOpen()) {
shellPanel.close();
} else {
shellPanel.open();
}
return true;
}

// F2 opens actions menu (global)
if (ke.isKey(KeyCode.F2)) {
if (tabsState.selected() == TAB_ROUTES && routesTab != null) {
Expand Down Expand Up @@ -643,6 +659,10 @@ private boolean handleEvent(Event event, TuiRunner runner) {
return true;
}
}
if (event instanceof MouseEvent) {
// Consume mouse events (scroll, click) — not forwarded to shell or tabs
return true;
}
if (event instanceof PasteEvent pe) {
if (actionsPopup.isVisible()) {
actionsPopup.handlePaste(pe.text());
Expand Down Expand Up @@ -891,13 +911,23 @@ private void render(Frame frame) {
// mainChunks.get(1) is the empty spacer row
renderTabs(frame, mainChunks.get(2));
// mainChunks.get(3) is the empty spacer row between tabs and content
renderContent(frame, mainChunks.get(4));
if (showKillConfirm) {
renderKillConfirm(frame, mainChunks.get(4));
}
actionsPopup.render(frame, mainChunks.get(4));
if (captionOverlay.isCaptionVisible()) {
captionOverlay.render(frame, mainChunks.get(4));
Rect contentArea = mainChunks.get(4);
if (shellPanel.isOpen()) {
List<Rect> splitChunks = Layout.vertical()
.constraints(Constraint.percentage(50), Constraint.percentage(50))
.split(contentArea);
renderContent(frame, splitChunks.get(0));
shellPanel.render(frame, splitChunks.get(1));
actionsPopup.render(frame, splitChunks.get(0));
} else {
renderContent(frame, contentArea);
if (showKillConfirm) {
renderKillConfirm(frame, contentArea);
}
actionsPopup.render(frame, contentArea);
if (captionOverlay.isCaptionVisible()) {
captionOverlay.render(frame, contentArea);
}
}
renderFooter(frame, mainChunks.get(5));

Expand Down Expand Up @@ -1901,6 +1931,12 @@ private void renderFooter(Frame frame, Rect area) {
return;
}

if (shellPanel.isOpen()) {
shellPanel.renderFooter(spans);
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
return;
}

MonitorTab tab = activeTab();

if (tab != null) {
Expand Down
Loading