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) {
