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

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

commit 0afb7bdc8f4d88519c1056f5308f3f95a3da6bea
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 29 15:43:30 2026 +0200

    CAMEL-23855: camel-jbang - AI panel improvements and AI Log popup
    
    - Add markdown rendering with hard breaks for LLM responses
    - Add auto-scroll to bottom on new question/response
    - Add scrollbar when conversation overflows
    - Add dimmed placeholder text
    - Add AI Log popup (Actions menu) showing tool calls, args, results
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  33 ++-
 .../dsl/jbang/core/commands/tui/AiLogPopup.java    | 222 +++++++++++++++++++++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 120 ++++++++++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   1 +
 4 files changed, 365 insertions(+), 11 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 6f57ad1ec389..d90a0efa92eb 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
@@ -87,11 +87,12 @@ class ActionsPopup {
         SETUP_AI,
         MCP_INFO,
         MCP_LOG,
+        AI_LOG,
         SHELL
     }
 
     private static final int[] GROUP_SIZES = { 5, 4, 5 };
-    private static final int MCP_GROUP_SIZE = 3;
+    private static final int MCP_GROUP_SIZE = 4;
     private static final int SHELL_GROUP_SIZE = 1;
 
     private final Supplier<Set<String>> runningNames;
@@ -148,6 +149,7 @@ class ActionsPopup {
     private String selectedFolder;
 
     private final McpLogPopup mcpLogPopup = new McpLogPopup();
+    private final AiLogPopup aiLogPopup = new AiLogPopup();
 
     private final DoctorPopup doctorPopup = new DoctorPopup();
     private final SendMessagePopup sendMessagePopup = new SendMessagePopup();
@@ -218,6 +220,10 @@ class ActionsPopup {
         mcpLogPopup.setActivityLog(activityLog);
     }
 
+    void setAiActivityLog(Supplier<List<AiPanel.LogEntry>> activityLog) {
+        aiLogPopup.setActivityLog(activityLog);
+    }
+
     private int visualActionCount() {
         int total = 0;
         for (int gs : GROUP_SIZES) {
@@ -273,7 +279,7 @@ class ActionsPopup {
                 Action.SHOW_KEYSTROKES));
         if (mcpEnabled) {
             flat.add(null);
-            flat.addAll(List.of(Action.SETUP_AI, Action.MCP_INFO, 
Action.MCP_LOG));
+            flat.addAll(List.of(Action.SETUP_AI, Action.MCP_INFO, 
Action.MCP_LOG, Action.AI_LOG));
         }
         flat.add(null);
         flat.add(Action.SHELL);
@@ -298,7 +304,7 @@ class ActionsPopup {
         return showActionsMenu || showExampleBrowser || showFolderInput || 
runOptionsForm.isVisible()
                 || showDocPicker || showDocViewer
                 || showInfraBrowser || showInfraPortDialog
-                || mcpLogPopup.isVisible() || doctorPopup.isVisible()
+                || mcpLogPopup.isVisible() || aiLogPopup.isVisible() || 
doctorPopup.isVisible()
                 || sendMessagePopup.isVisible() || stopAllPopup.isVisible() || 
captionOverlay.isInlineMode();
     }
 
@@ -366,6 +372,7 @@ class ActionsPopup {
             labels.add("Setup AI...");
             labels.add("MCP Info");
             labels.add("MCP Log");
+            labels.add("AI Log");
         }
         labels.add("───");
         labels.add("Shell");
@@ -387,6 +394,7 @@ class ActionsPopup {
         showInfraBrowser = false;
         showInfraPortDialog = false;
         mcpLogPopup.close();
+        aiLogPopup.close();
         doctorPopup.close();
         sendMessagePopup.close();
         stopAllPopup.close();
@@ -419,6 +427,9 @@ class ActionsPopup {
         if (mcpLogPopup.handleKeyEvent(ke)) {
             return true;
         }
+        if (aiLogPopup.handleKeyEvent(ke)) {
+            return true;
+        }
         if (showDocViewer) {
             if (ke.isCancel()) {
                 showDocViewer = false;
@@ -608,6 +619,9 @@ class ActionsPopup {
                     } else if (action == Action.MCP_LOG) {
                         showActionsMenu = false;
                         openMcpLog();
+                    } else if (action == Action.AI_LOG) {
+                        showActionsMenu = false;
+                        openAiLog();
                     } else if (action == Action.SEND_MESSAGE) {
                         showActionsMenu = false;
                         openSendMessage();
@@ -664,6 +678,9 @@ class ActionsPopup {
         if (mcpLogPopup.isVisible()) {
             mcpLogPopup.render(frame, area);
         }
+        if (aiLogPopup.isVisible()) {
+            aiLogPopup.render(frame, area);
+        }
         if (doctorPopup.isVisible()) {
             doctorPopup.render(frame, area);
         }
@@ -695,6 +712,10 @@ class ActionsPopup {
             doctorPopup.renderFooter(spans);
             return;
         }
+        if (aiLogPopup.isVisible()) {
+            aiLogPopup.renderFooter(spans);
+            return;
+        }
         if (mcpLogPopup.isVisible()) {
             mcpLogPopup.renderFooter(spans);
             return;
@@ -809,6 +830,7 @@ class ActionsPopup {
             items.add(ListItem.from("  🧠 Setup AI..."));
             items.add(ListItem.from("  🤖 MCP Info"));
             items.add(ListItem.from("  📋 MCP Log"));
+            items.add(ListItem.from("  💬 AI Log"));
         }
         // Group 5: Shell
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
@@ -1245,6 +1267,10 @@ class ActionsPopup {
         mcpLogPopup.open();
     }
 
+    private void openAiLog() {
+        aiLogPopup.open();
+    }
+
     // ---- Folder Input ----
 
     private void openFolderInput() {
@@ -2171,6 +2197,7 @@ class ActionsPopup {
             case SETUP_AI -> openSetupAI();
             case MCP_INFO -> openMcpInfo();
             case MCP_LOG -> openMcpLog();
+            case AI_LOG -> openAiLog();
             default -> {
                 return false;
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
new file mode 100644
index 000000000000..147c75a51a69
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
@@ -0,0 +1,222 @@
+/*
+ * 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.Borders;
+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 AiLogPopup {
+
+    private boolean visible;
+    private Supplier<List<AiPanel.LogEntry>> activityLog;
+    private List<AiPanel.LogEntry> entries;
+    private int selected;
+    private int detailScroll;
+
+    void setActivityLog(Supplier<List<AiPanel.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).borders(Borders.ALL)
+                    .title(" AI 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 AI activity yet. Open the AI panel (F8) 
and ask a question.", 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 (AiPanel.LogEntry entry : entries) {
+            Style levelStyle = switch (entry.level()) {
+                case QUESTION -> Style.EMPTY.fg(Color.CYAN);
+                case TOOL -> Style.EMPTY.fg(Color.YELLOW);
+                case RESULT -> Style.EMPTY.fg(Color.GREEN);
+                case RESPONSE -> Style.EMPTY.fg(Color.MAGENTA);
+                case ERROR -> Style.EMPTY.fg(Color.LIGHT_RED);
+            };
+            String levelTag = switch (entry.level()) {
+                case QUESTION -> " ASK      ";
+                case TOOL -> " TOOL     ";
+                case RESULT -> " RESULT   ";
+                case RESPONSE -> " RESPONSE ";
+                case ERROR -> " ERROR    ";
+            };
+            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).borders(Borders.ALL)
+                        .title(" AI Log ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, area, masterState);
+    }
+
+    private void renderDetail(Frame frame, Rect area) {
+        AiPanel.LogEntry entry = entries.get(selected);
+        List<Line> lines = new ArrayList<>();
+
+        String detail = entry.detail();
+        if (detail != null && !detail.isBlank()) {
+            if (entry.level() == AiPanel.LogLevel.TOOL || entry.level() == 
AiPanel.LogLevel.RESULT) {
+                lines.add(Line.from(Span.styled(
+                        entry.level() == AiPanel.LogLevel.TOOL ? "▶ Arguments" 
: "◀ Result",
+                        Style.EMPTY.fg(entry.level() == AiPanel.LogLevel.TOOL 
? Color.YELLOW : Color.GREEN).bold())));
+                addJsonLines(lines, detail);
+            } else {
+                lines.add(Line.from(Span.styled("▶ Content",
+                        Style.EMPTY.fg(Color.CYAN).bold())));
+                for (String line : detail.split("\n", -1)) {
+                    lines.add(Line.from(Span.styled("  " + line, 
Style.EMPTY.dim())));
+                }
+            }
+        } else {
+            lines.add(Line.from(Span.styled("(no detail data)", 
Style.EMPTY.dim())));
+        }
+
+        Block detailBlock = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .title(" Detail ")
+                .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) {
+        try {
+            String pretty = Jsoner.prettyPrint(json, 2);
+            for (String line : pretty.split("\n", -1)) {
+                lines.add(Line.from(Span.styled("  " + line, 
Style.EMPTY.dim())));
+            }
+        } catch (Exception e) {
+            for (String line : json.split("\n", -1)) {
+                lines.add(Line.from(Span.styled("  " + line, 
Style.EMPTY.dim())));
+            }
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 52d2257bd676..cb38d8dd4cf3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -47,6 +50,20 @@ class AiPanel {
 
     private static final int[] SPLIT_PERCENTS = { 25, 50, 75, 100 };
     private static final int MAX_ITERATIONS = 10;
+    private static final int MAX_LOG_ENTRIES = 200;
+    private static final DateTimeFormatter TIME_FMT
+            = 
DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    enum LogLevel {
+        QUESTION,
+        TOOL,
+        RESULT,
+        RESPONSE,
+        ERROR
+    }
+
+    record LogEntry(String timestamp, LogLevel level, String message, String 
detail) {
+    }
 
     private boolean visible;
     private int splitIndex = 1; // default 50%
@@ -69,6 +86,9 @@ class AiPanel {
     private volatile Thread agentThread;
     private String initError;
 
+    // Activity log for AI Log popup
+    private final List<LogEntry> activityLog = new ArrayList<>();
+
     record ConversationEntry(String role, String text) {
     }
 
@@ -76,6 +96,17 @@ class AiPanel {
         this.ctx = ctx;
     }
 
+    synchronized List<LogEntry> getActivityLog() {
+        return new ArrayList<>(activityLog);
+    }
+
+    private synchronized void log(LogLevel level, String message, String 
detail) {
+        activityLog.add(new LogEntry(TIME_FMT.format(Instant.now()), level, 
message, detail));
+        if (activityLog.size() > MAX_LOG_ENTRIES) {
+            activityLog.remove(0);
+        }
+    }
+
     boolean isOpen() {
         return visible;
     }
@@ -209,6 +240,7 @@ class AiPanel {
         scrollOffset = 0;
 
         conversation.add(new ConversationEntry("user", question));
+        log(LogLevel.QUESTION, "Question", question);
         thinking.set(true);
 
         // rebuild tools if target process changed
@@ -247,7 +279,9 @@ class AiPanel {
 
             LlmClient.ChatResponse response = 
client.chatWithTools(systemPrompt, messages, tools);
             if (response == null) {
-                conversation.add(new ConversationEntry("error", "No response 
from LLM"));
+                String err = "No response from LLM";
+                conversation.add(new ConversationEntry("error", err));
+                log(LogLevel.ERROR, "Error", err);
                 return;
             }
 
@@ -255,7 +289,9 @@ class AiPanel {
             if ("error".equals(response.stopReason())
                     && (response.toolCalls() == null || 
response.toolCalls().isEmpty())
                     && response.text() == null) {
-                conversation.add(new ConversationEntry("error", "LLM request 
failed. Check API key and endpoint."));
+                String err = "LLM request failed. Check API key and endpoint.";
+                conversation.add(new ConversationEntry("error", err));
+                log(LogLevel.ERROR, "Error", err);
                 return;
             }
 
@@ -267,7 +303,9 @@ class AiPanel {
                     if (Thread.interrupted()) {
                         throw new InterruptedException();
                     }
+                    log(LogLevel.TOOL, toolCall.name(), 
toolCall.arguments().toJson());
                     String result = askTools.executeTool(toolCall.name(), 
toolCall.arguments());
+                    log(LogLevel.RESULT, toolCall.name(), result);
                     results.add(new LlmClient.ToolResult(toolCall.id(), 
result));
                 }
                 messages.add(LlmClient.Message.toolResults(results));
@@ -275,9 +313,13 @@ class AiPanel {
                 String text = response.text();
                 if (text != null && !text.isBlank()) {
                     conversation.add(new ConversationEntry("assistant", text));
+                    log(LogLevel.RESPONSE, "Response", text);
                 } else {
-                    conversation.add(new ConversationEntry("error", "Empty 
response from LLM."));
+                    String err = "Empty response from LLM.";
+                    conversation.add(new ConversationEntry("error", err));
+                    log(LogLevel.ERROR, "Error", err);
                 }
+                scrollOffset = 0;
                 messages.add(LlmClient.Message.assistantWithToolCalls(text, 
List.of()));
                 return;
             }
@@ -326,13 +368,16 @@ class AiPanel {
         if (initError != null) {
             md.append("**Error:** ").append(initError).append("\n\n");
         } else if (conversation.isEmpty() && !thinking.get()) {
-            md.append("*Ask a question about your Camel application...*\n");
+            frame.renderWidget(
+                    Paragraph.from(Line.from(Span.styled("Ask a question about 
your Camel application...", Style.EMPTY.dim()))),
+                    area);
+            return;
         }
 
         for (ConversationEntry entry : conversation) {
             switch (entry.role()) {
                 case "user" -> md.append("**You:** 
").append(entry.text()).append("\n\n");
-                case "assistant" -> md.append(entry.text()).append("\n\n");
+                case "assistant" -> 
md.append(toHardBreaks(entry.text())).append("\n\n");
                 case "error" -> md.append("**Error:** 
").append(entry.text()).append("\n\n");
                 case "system" -> 
md.append("*").append(entry.text()).append("*\n\n");
                 default -> {
@@ -345,11 +390,44 @@ class AiPanel {
             md.append("*🤔 thinking").append(".".repeat((int) dots + 
1)).append("*\n");
         }
 
+        String source = md.toString();
+
+        // Estimate total rendered lines (accounting for word wrap)
+        int contentWidth = Math.max(1, area.width());
+        int estimatedLines = 0;
+        for (String l : source.split("\n", -1)) {
+            estimatedLines += Math.max(1, (l.length() / contentWidth) + 1);
+        }
+
+        boolean overflow = estimatedLines > area.height();
+        Rect contentArea = area;
+        Rect scrollbarArea = null;
+        if (overflow) {
+            List<Rect> hParts = Layout.horizontal()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(area);
+            contentArea = hParts.get(0);
+            scrollbarArea = hParts.get(1);
+        }
+
+        // scrollOffset=0 means auto-scroll to bottom (most recent content 
visible)
+        // scrollOffset>0 means user scrolled up by that many lines
+        int scroll;
+        if (scrollOffset == 0) {
+            scroll = estimatedLines;
+        } else {
+            scroll = Math.max(0, estimatedLines - contentArea.height() - 
scrollOffset);
+        }
+
         MarkdownView view = MarkdownView.builder()
-                .source(md.toString())
-                .scroll(scrollOffset)
+                .source(source)
+                .scroll(scroll)
                 .build();
-        frame.renderWidget(view, area);
+        frame.renderWidget(view, contentArea);
+
+        if (overflow && scrollbarArea != null) {
+            renderScrollbar(frame, scrollbarArea, estimatedLines, 
contentArea.height(), scroll);
+        }
     }
 
     private void renderInput(Frame frame, Rect area) {
@@ -403,4 +481,30 @@ class AiPanel {
         }
     }
 
+    private void renderScrollbar(Frame frame, Rect area, int totalLines, int 
visibleHeight, int scroll) {
+        int thumbSize = Math.max(1, visibleHeight * visibleHeight / 
Math.max(1, totalLines));
+        int maxScroll = Math.max(1, totalLines - visibleHeight);
+        int thumbPos = (int) ((long) Math.min(scroll, maxScroll) * 
(visibleHeight - thumbSize) / maxScroll);
+
+        List<Line> lines = new ArrayList<>();
+        for (int i = 0; i < area.height(); i++) {
+            if (i >= thumbPos && i < thumbPos + thumbSize) {
+                lines.add(Line.from(Span.styled("▐", 
Style.EMPTY.fg(Color.CYAN))));
+            } else {
+                lines.add(Line.from(Span.styled("│", Style.EMPTY.dim())));
+            }
+        }
+        frame.renderWidget(Paragraph.from(new dev.tamboui.text.Text(lines, 
dev.tamboui.layout.Alignment.LEFT)), area);
+    }
+
+    private static String toHardBreaks(String text) {
+        if (text == null) {
+            return "";
+        }
+        // Convert single newlines to markdown hard breaks (two trailing 
spaces + newline)
+        // so the LLM's line-by-line formatting is preserved in MarkdownView.
+        // Double newlines (paragraph breaks) are left as-is.
+        return text.replaceAll("(?<!\n)\n(?!\n)", "  \n");
+    }
+
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 1be046368239..94b4472a1e7c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -324,6 +324,7 @@ public class CamelMonitor extends CamelCommand {
             try {
                 mcpServer.start();
                 actionsPopup.setMcpEnabled(true, mcpPort, 
mcpServer::getConnectedClient, mcpServer::getActivityLog);
+                actionsPopup.setAiActivityLog(aiPanel::getActivityLog);
                 mcpJsonFile = writeMcpJson(mcpPort);
             } catch (java.net.BindException e) {
                 System.err.println("MCP server failed to start: port " + 
mcpPort + " is already in use.");

Reply via email to