This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch tui-mcp-CAMEL-23606
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e42e47310cb7944d24b444d33d9e6de04ae2eca6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun May 24 22:56:03 2026 +0200

    CAMEL-23606: camel-tui - extract McpLogPopup from ActionsPopup
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 148 +--------------
 .../dsl/jbang/core/commands/tui/McpLogPopup.java   | 210 +++++++++++++++++++++
 2 files changed, 220 insertions(+), 138 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 e04744fcdb82..8a36f8a2f907 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
@@ -49,7 +49,6 @@ 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.util.json.JsonObject;
-import org.apache.camel.util.json.Jsoner;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
 import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
@@ -98,10 +97,7 @@ class ActionsPopup {
     private String docTitle;
     private int docScroll;
 
-    private boolean showMcpLog;
-    private List<TuiMcpServer.LogEntry> mcpLogEntries;
-    private int mcpLogSelected;
-    private int mcpLogDetailScroll;
+    private final McpLogPopup mcpLogPopup = new McpLogPopup();
 
     private final DoctorPopup doctorPopup = new DoctorPopup();
     private final ClasspathPopup classpathPopup = new ClasspathPopup();
@@ -135,6 +131,7 @@ class ActionsPopup {
         this.mcpPort = port;
         this.mcpConnectedClient = connectedClient;
         this.mcpActivityLog = activityLog;
+        mcpLogPopup.setActivityLog(activityLog);
     }
 
     private int actionCount() {
@@ -143,7 +140,7 @@ class ActionsPopup {
 
     boolean isVisible() {
         return showActionsMenu || showExampleBrowser || 
runOptionsForm.isVisible() || showDocPicker || showDocViewer
-                || showMcpLog || doctorPopup.isVisible() || 
classpathPopup.isVisible()
+                || mcpLogPopup.isVisible() || doctorPopup.isVisible() || 
classpathPopup.isVisible()
                 || stopAllPopup.isVisible() || captionOverlay.isInputVisible();
     }
 
@@ -158,7 +155,7 @@ class ActionsPopup {
         runOptionsForm.close();
         showDocPicker = false;
         showDocViewer = false;
-        showMcpLog = false;
+        mcpLogPopup.close();
         doctorPopup.close();
         classpathPopup.close();
         stopAllPopup.close();
@@ -174,24 +171,7 @@ class ActionsPopup {
     }
 
     boolean handleKeyEvent(KeyEvent ke) {
-        if (showMcpLog) {
-            if (ke.isCancel()) {
-                showMcpLog = false;
-            } else if (ke.isUp() || ke.isChar('k')) {
-                if (mcpLogEntries != null && !mcpLogEntries.isEmpty()) {
-                    mcpLogSelected = Math.max(0, mcpLogSelected - 1);
-                    mcpLogDetailScroll = 0;
-                }
-            } else if (ke.isDown() || ke.isChar('j')) {
-                if (mcpLogEntries != null && !mcpLogEntries.isEmpty()) {
-                    mcpLogSelected = Math.min(mcpLogEntries.size() - 1, 
mcpLogSelected + 1);
-                    mcpLogDetailScroll = 0;
-                }
-            } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
-                mcpLogDetailScroll = Math.max(0, mcpLogDetailScroll - 5);
-            } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
-                mcpLogDetailScroll += 5;
-            }
+        if (mcpLogPopup.handleKeyEvent(ke)) {
             return true;
         }
         if (showDocViewer) {
@@ -334,8 +314,8 @@ class ActionsPopup {
         if (showDocViewer) {
             renderDocViewer(frame, area);
         }
-        if (showMcpLog) {
-            renderMcpLog(frame, area);
+        if (mcpLogPopup.isVisible()) {
+            mcpLogPopup.render(frame, area);
         }
         if (doctorPopup.isVisible()) {
             doctorPopup.render(frame, area);
@@ -368,10 +348,8 @@ class ActionsPopup {
             doctorPopup.renderFooter(spans);
             return;
         }
-        if (showMcpLog) {
-            hint(spans, "↑↓", "select");
-            hint(spans, "PgUp/Dn", "scroll detail");
-            hintLast(spans, "Esc", "back");
+        if (mcpLogPopup.isVisible()) {
+            mcpLogPopup.renderFooter(spans);
             return;
         }
         if (showDocViewer) {
@@ -768,113 +746,7 @@ class ActionsPopup {
     }
 
     private void openMcpLog() {
-        mcpLogEntries = mcpActivityLog != null ? mcpActivityLog.get() : 
List.of();
-        mcpLogSelected = mcpLogEntries.isEmpty() ? 0 : mcpLogEntries.size() - 
1;
-        mcpLogDetailScroll = 0;
-        showMcpLog = true;
-    }
-
-    private void renderMcpLog(Frame frame, Rect area) {
-        Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() - 
4, area.height() - 2);
-        frame.renderWidget(Clear.INSTANCE, popup);
-
-        if (mcpLogEntries == null || mcpLogEntries.isEmpty()) {
-            Block block = Block.builder()
-                    .borderType(BorderType.ROUNDED)
-                    .title(" MCP Log ")
-                    .titleBottom(Title.from(Line.from(
-                            Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
-                    .build();
-            frame.renderWidget(block, popup);
-            Rect inner = block.inner(popup);
-            frame.renderWidget(Paragraph.from(Line.from(
-                    Span.styled("No MCP activity yet.", Style.EMPTY.dim()))), 
inner);
-            return;
-        }
-
-        int splitY = popup.top() + Math.max(3, (popup.height() * 2) / 5);
-        Rect masterArea = new Rect(popup.left(), popup.top(), popup.width(), 
splitY - popup.top());
-        Rect detailArea = new Rect(popup.left(), splitY, popup.width(), 
popup.bottom() - splitY);
-
-        // Master: log entry list
-        List<ListItem> items = new ArrayList<>();
-        for (TuiMcpServer.LogEntry entry : mcpLogEntries) {
-            Style levelStyle = switch (entry.level()) {
-                case CONNECT -> Style.EMPTY.fg(Color.GREEN);
-                case TOOL -> Style.EMPTY.fg(Color.CYAN);
-                case ERROR -> Style.EMPTY.fg(Color.LIGHT_RED);
-                default -> Style.EMPTY.fg(Color.GREEN);
-            };
-            String levelTag = switch (entry.level()) {
-                case CONNECT -> " CONNECT ";
-                case TOOL -> " TOOL    ";
-                case ERROR -> " ERROR   ";
-                default -> " INFO    ";
-            };
-            items.add(ListItem.from(Line.from(
-                    Span.styled(entry.timestamp(), Style.EMPTY.dim()),
-                    Span.styled(levelTag, levelStyle),
-                    Span.raw(entry.message()))));
-        }
-
-        ListState masterState = new ListState();
-        masterState.select(mcpLogSelected);
-        ListWidget list = ListWidget.builder()
-                .items(items.toArray(ListItem[]::new))
-                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
-                .highlightSymbol("▸ ")
-                .scrollMode(ScrollMode.AUTO_SCROLL)
-                .block(Block.builder()
-                        .borderType(BorderType.ROUNDED)
-                        .title(" MCP Log ")
-                        .build())
-                .build();
-        frame.renderStatefulWidget(list, masterArea, masterState);
-
-        // Detail: request + response JSON
-        TuiMcpServer.LogEntry selected = mcpLogEntries.get(mcpLogSelected);
-        List<Line> detailLines = new ArrayList<>();
-        if (selected.requestBody() != null) {
-            detailLines.add(Line.from(Span.styled("▶ Request", 
Style.EMPTY.fg(Color.YELLOW).bold())));
-            addJsonLines(detailLines, selected.requestBody());
-            detailLines.add(Line.from(Span.raw("")));
-        }
-        if (selected.responseBody() != null) {
-            detailLines.add(Line.from(Span.styled("◀ Response", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            addJsonLines(detailLines, selected.responseBody());
-        }
-        if (selected.requestBody() == null && selected.responseBody() == null) 
{
-            detailLines.add(Line.from(Span.styled("(no request/response 
data)", Style.EMPTY.dim())));
-        }
-
-        Block detailBlock = Block.builder()
-                .borderType(BorderType.ROUNDED)
-                .title(" Detail ")
-                .titleBottom(Title.from(Line.from(
-                        Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" select │"),
-                        Span.styled(" PgUp/Dn", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" scroll │"),
-                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
-                .build();
-        frame.renderWidget(detailBlock, detailArea);
-        Rect detailInner = detailBlock.inner(detailArea);
-
-        int visibleLines = detailInner.height();
-        int totalLines = detailLines.size();
-        int clampedScroll = Math.min(mcpLogDetailScroll, Math.max(0, 
totalLines - visibleLines));
-        int end = Math.min(clampedScroll + visibleLines, totalLines);
-        if (clampedScroll < end) {
-            List<Line> visible = detailLines.subList(clampedScroll, end);
-            frame.renderWidget(
-                    
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
-                    detailInner);
-        }
-    }
-
-    private static void addJsonLines(List<Line> lines, String json) {
-        String pretty = Jsoner.prettyPrint(json, 2);
-        for (String line : pretty.split("\n", -1)) {
-            lines.add(Line.from(Span.styled("  " + line, Style.EMPTY.dim())));
-        }
+        mcpLogPopup.open();
     }
 
     private void openClasspath() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
new file mode 100644
index 000000000000..def5d1c5f3d4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
@@ -0,0 +1,210 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+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.text.Text;
+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.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;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.util.json.Jsoner;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class McpLogPopup {
+
+    private boolean visible;
+    private Supplier<List<TuiMcpServer.LogEntry>> activityLog;
+    private List<TuiMcpServer.LogEntry> entries;
+    private int selected;
+    private int detailScroll;
+
+    void setActivityLog(Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
+        this.activityLog = activityLog;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open() {
+        entries = activityLog != null ? activityLog.get() : List.of();
+        selected = entries.isEmpty() ? 0 : entries.size() - 1;
+        detailScroll = 0;
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+        } else if (ke.isUp() || ke.isChar('k')) {
+            if (entries != null && !entries.isEmpty()) {
+                selected = Math.max(0, selected - 1);
+                detailScroll = 0;
+            }
+        } else if (ke.isDown() || ke.isChar('j')) {
+            if (entries != null && !entries.isEmpty()) {
+                selected = Math.min(entries.size() - 1, selected + 1);
+                detailScroll = 0;
+            }
+        } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            detailScroll = Math.max(0, detailScroll - 5);
+        } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            detailScroll += 5;
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() - 
4, area.height() - 2);
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        if (entries == null || entries.isEmpty()) {
+            Block block = Block.builder()
+                    .borderType(BorderType.ROUNDED)
+                    .title(" MCP Log ")
+                    .titleBottom(Title.from(Line.from(
+                            Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+                    .build();
+            frame.renderWidget(block, popup);
+            Rect inner = block.inner(popup);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled("No MCP activity yet.", Style.EMPTY.dim()))), 
inner);
+            return;
+        }
+
+        int splitY = popup.top() + Math.max(3, (popup.height() * 2) / 5);
+        Rect masterArea = new Rect(popup.left(), popup.top(), popup.width(), 
splitY - popup.top());
+        Rect detailArea = new Rect(popup.left(), splitY, popup.width(), 
popup.bottom() - splitY);
+
+        renderMaster(frame, masterArea);
+        renderDetail(frame, detailArea);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "↑↓", "select");
+        hint(spans, "PgUp/Dn", "scroll detail");
+        hintLast(spans, "Esc", "back");
+    }
+
+    private void renderMaster(Frame frame, Rect area) {
+        List<ListItem> items = new ArrayList<>();
+        for (TuiMcpServer.LogEntry entry : entries) {
+            Style levelStyle = switch (entry.level()) {
+                case CONNECT -> Style.EMPTY.fg(Color.GREEN);
+                case TOOL -> Style.EMPTY.fg(Color.CYAN);
+                case ERROR -> Style.EMPTY.fg(Color.LIGHT_RED);
+                default -> Style.EMPTY.fg(Color.GREEN);
+            };
+            String levelTag = switch (entry.level()) {
+                case CONNECT -> " CONNECT ";
+                case TOOL -> " TOOL    ";
+                case ERROR -> " ERROR   ";
+                default -> " INFO    ";
+            };
+            items.add(ListItem.from(Line.from(
+                    Span.styled(entry.timestamp(), Style.EMPTY.dim()),
+                    Span.styled(levelTag, levelStyle),
+                    Span.raw(entry.message()))));
+        }
+
+        ListState masterState = new ListState();
+        masterState.select(selected);
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("▸ ")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" MCP Log ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, area, masterState);
+    }
+
+    private void renderDetail(Frame frame, Rect area) {
+        TuiMcpServer.LogEntry entry = entries.get(selected);
+        List<Line> lines = new ArrayList<>();
+        if (entry.requestBody() != null) {
+            lines.add(Line.from(Span.styled("▶ Request", 
Style.EMPTY.fg(Color.YELLOW).bold())));
+            addJsonLines(lines, entry.requestBody());
+            lines.add(Line.from(Span.raw("")));
+        }
+        if (entry.responseBody() != null) {
+            lines.add(Line.from(Span.styled("◀ Response", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            addJsonLines(lines, entry.responseBody());
+        }
+        if (entry.requestBody() == null && entry.responseBody() == null) {
+            lines.add(Line.from(Span.styled("(no request/response data)", 
Style.EMPTY.dim())));
+        }
+
+        Block detailBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Detail ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" select │"),
+                        Span.styled(" PgUp/Dn", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" scroll │"),
+                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
+                .build();
+        frame.renderWidget(detailBlock, area);
+        Rect inner = detailBlock.inner(area);
+
+        int visibleLines = inner.height();
+        int totalLines = lines.size();
+        int clampedScroll = Math.min(detailScroll, Math.max(0, totalLines - 
visibleLines));
+        int end = Math.min(clampedScroll + visibleLines, totalLines);
+        if (clampedScroll < end) {
+            List<Line> visible = lines.subList(clampedScroll, end);
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
+                    inner);
+        }
+    }
+
+    private static void addJsonLines(List<Line> lines, String json) {
+        String pretty = Jsoner.prettyPrint(json, 2);
+        for (String line : pretty.split("\n", -1)) {
+            lines.add(Line.from(Span.styled("  " + line, Style.EMPTY.dim())));
+        }
+    }
+}

Reply via email to