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 43641866803c5d753a6499c681c8a627d1b865fa
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 29 17:52:47 2026 +0200

    CAMEL-23855: camel-jbang - AI panel scroll fix and dimmed elapsed time
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 64 +++++++++++++++++-----
 1 file changed, 50 insertions(+), 14 deletions(-)

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 cb38d8dd4cf3..e083564269f6 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
@@ -85,11 +85,15 @@ class AiPanel {
     private final AtomicBoolean thinking = new AtomicBoolean();
     private volatile Thread agentThread;
     private String initError;
+    private long thinkingStartTime;
 
     // Activity log for AI Log popup
     private final List<LogEntry> activityLog = new ArrayList<>();
 
-    record ConversationEntry(String role, String text) {
+    record ConversationEntry(String role, String text, long elapsedSeconds) {
+        ConversationEntry(String role, String text) {
+            this(role, text, -1);
+        }
     }
 
     void setContext(MonitorContext ctx) {
@@ -241,6 +245,7 @@ class AiPanel {
 
         conversation.add(new ConversationEntry("user", question));
         log(LogLevel.QUESTION, "Question", question);
+        thinkingStartTime = System.currentTimeMillis();
         thinking.set(true);
 
         // rebuild tools if target process changed
@@ -312,8 +317,9 @@ class AiPanel {
             } else {
                 String text = response.text();
                 if (text != null && !text.isBlank()) {
-                    conversation.add(new ConversationEntry("assistant", text));
-                    log(LogLevel.RESPONSE, "Response", text);
+                    long elapsed = (System.currentTimeMillis() - 
thinkingStartTime) / 1000;
+                    conversation.add(new ConversationEntry("assistant", text, 
elapsed));
+                    log(LogLevel.RESPONSE, "Response (" + elapsed + "s)", 
text);
                 } else {
                     String err = "Empty response from LLM.";
                     conversation.add(new ConversationEntry("error", err));
@@ -386,38 +392,62 @@ class AiPanel {
         }
 
         if (thinking.get()) {
+            long elapsed = (System.currentTimeMillis() - thinkingStartTime) / 
1000;
             long dots = (System.currentTimeMillis() / 500) % 4;
-            md.append("*🤔 thinking").append(".".repeat((int) dots + 
1)).append("*\n");
+            md.append("*🤔 thinking");
+            if (elapsed > 0) {
+                md.append(" (").append(elapsed).append("s)");
+            }
+            md.append(".".repeat((int) dots + 1)).append("*\n");
+        }
+
+        // Show elapsed time as a dimmed line below the markdown when at the 
bottom
+        long lastElapsed = -1;
+        if (!thinking.get() && !conversation.isEmpty()) {
+            ConversationEntry last = conversation.get(conversation.size() - 1);
+            if ("assistant".equals(last.role()) && last.elapsedSeconds() >= 0) 
{
+                lastElapsed = last.elapsedSeconds();
+            }
+        }
+
+        // Reserve 1 row for dimmed elapsed time when we have one to show
+        Rect mdArea = area;
+        Rect elapsedArea = null;
+        if (lastElapsed >= 0 && area.height() > 2) {
+            List<Rect> vParts = Layout.vertical()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(area);
+            mdArea = vParts.get(0);
+            elapsedArea = vParts.get(1);
         }
 
         String source = md.toString();
 
         // Estimate total rendered lines (accounting for word wrap)
-        int contentWidth = Math.max(1, area.width());
+        int contentWidth = Math.max(1, mdArea.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;
+        boolean overflow = estimatedLines > mdArea.height();
+        Rect contentArea = mdArea;
         Rect scrollbarArea = null;
         if (overflow) {
             List<Rect> hParts = Layout.horizontal()
                     .constraints(Constraint.fill(), Constraint.length(1))
-                    .split(area);
+                    .split(mdArea);
             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);
-        }
+        // Clamp so PgDn always has immediate effect after scrolling past the 
top
+        int maxScrollOffset = Math.max(0, estimatedLines - 
contentArea.height());
+        scrollOffset = Math.min(scrollOffset, maxScrollOffset);
+
+        int scroll = Math.max(0, maxScrollOffset - scrollOffset);
 
         MarkdownView view = MarkdownView.builder()
                 .source(source)
@@ -428,6 +458,12 @@ class AiPanel {
         if (overflow && scrollbarArea != null) {
             renderScrollbar(frame, scrollbarArea, estimatedLines, 
contentArea.height(), scroll);
         }
+
+        if (elapsedArea != null && lastElapsed >= 0) {
+            frame.renderWidget(
+                    Paragraph.from(Line.from(Span.styled("(" + lastElapsed + 
"s)", Style.EMPTY.dim()))),
+                    elapsedArea);
+        }
     }
 
     private void renderInput(Frame frame, Rect area) {

Reply via email to