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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 964b6b7738c8 CAMEL-23860: Track LLM token usage in CLI ask and TUI 
(#24361)
964b6b7738c8 is described below

commit 964b6b7738c8216ef0188715061643fcf6212818
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jul 1 13:58:00 2026 +0200

    CAMEL-23860: Track LLM token usage in CLI ask and TUI (#24361)
    
    * CAMEL-23860: Track LLM token usage in CLI ask command and TUI AI panel
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23860: Add --show-stats option to camel ask for toggling token/time 
display
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23860: Default --show-stats to false for silent output by default
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23860: Regenerate docs for camel ask --show-stats option
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23860: Format token counts as compact numbers (5.4k instead of 5400)
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23860: Track accumulated session token total in TUI AI panel
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23860: Address review feedback
    
    - Use AtomicInteger for toolCallCount in TuiMcpServer (thread-safe 
increment)
    - Mark sessionTotalTokens volatile in AiPanel (cross-thread visibility)
    - Move formatTokens() to LlmClient to eliminate duplication between Ask and 
AiPanel
    - Regenerate commands metadata
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude <[email protected]>
---
 .../ROOT/pages/jbang-commands/camel-jbang-ask.adoc |   1 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../apache/camel/dsl/jbang/core/commands/Ask.java  |  26 ++++++
 .../camel/dsl/jbang/core/commands/LlmClient.java   | 103 +++++++++++++++++----
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |   4 +-
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  40 ++++++--
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   3 +-
 .../dsl/jbang/core/commands/tui/McpLogPopup.java   |  17 +++-
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  |   7 ++
 9 files changed, 174 insertions(+), 29 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc
index 0710cd347ae8..3cbefd8280fb 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-ask.adoc
@@ -24,6 +24,7 @@ camel ask [options]
 | `--max-iterations` | Maximum number of tool-calling rounds | 10 | int
 | `--model` | Model to use | DEFAULT_MODEL | String
 | `--name` | Name or PID of the Camel process. Auto-detected when exactly one 
process is running |  | String
+| `--show-stats` | Show token usage and elapsed time after response |  | 
boolean
 | `--show-tools` | Show tool calls and results as they happen |  | boolean
 | `--timeout` | Timeout in seconds for LLM response | 120 | int
 | `--url` | LLM API endpoint URL. Auto-detected if not specified. |  | String
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 93ef87cda9f2..9a8c2a2680a7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -1,6 +1,6 @@
 {
   "commands": [
-    { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
+    { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
     { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
index f6a3ad5d70ba..342c6eaca339 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
@@ -91,6 +91,10 @@ public class Ask extends CamelCommand {
             description = "Show tool calls and results as they happen")
     boolean showTools;
 
+    @Option(names = { "--show-stats" },
+            description = "Show token usage and elapsed time after response")
+    boolean showStats;
+
     @Option(names = { "--verbose" },
             description = "Print debug information: HTTP requests, responses, 
and parsed results")
     boolean verbose;
@@ -195,12 +199,15 @@ public class Ask extends CamelCommand {
             String userQuestion) {
         messages.add(LlmClient.Message.user(userQuestion));
 
+        long startTime = System.currentTimeMillis();
+        LlmClient.TokenUsage totalUsage = LlmClient.TokenUsage.EMPTY;
         for (int i = 0; i < maxIterations; i++) {
             LlmClient.ChatResponse response = 
client.chatWithTools(systemPrompt, messages, tools);
             if (response == null) {
                 printer().printErr("Failed to get response from LLM");
                 return 1;
             }
+            totalUsage = totalUsage.add(response.usage());
 
             if (response.toolCalls() != null && 
!response.toolCalls().isEmpty()) {
                 
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
response.toolCalls()));
@@ -222,14 +229,33 @@ public class Ask extends CamelCommand {
                     printer().println(response.text());
                 }
                 
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
List.of()));
+                printStats(totalUsage, startTime);
                 return 0;
             }
         }
 
+        printStats(totalUsage, startTime);
         printer().printErr("Reached maximum iterations (" + maxIterations + ") 
without a final answer.");
         return 1;
     }
 
+    private void printStats(LlmClient.TokenUsage usage, long startTime) {
+        if (!showStats) {
+            return;
+        }
+        long elapsed = (System.currentTimeMillis() - startTime) / 1000;
+        StringBuilder sb = new StringBuilder();
+        sb.append("(").append(elapsed).append("s");
+        if (usage.totalTokens() > 0) {
+            sb.append(", 
").append(LlmClient.formatTokens(usage.inputTokens())).append(" input / ")
+                    
.append(LlmClient.formatTokens(usage.outputTokens())).append(" output / ")
+                    
.append(LlmClient.formatTokens(usage.totalTokens())).append(" total tokens");
+        }
+        sb.append(")");
+        printer().println();
+        printer().println(sb.toString());
+    }
+
     // ---- Process discovery (delegates to RuntimeHelper) ----
 
     private RuntimeHelper.ProcessInfo findProcess(String nameOrPid) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
index d2abee4f8f44..63488519de91 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
@@ -90,7 +90,30 @@ public class LlmClient {
         }
     }
 
-    public record ChatResponse(String text, List<ToolCall> toolCalls, String 
stopReason, boolean streamed) {
+    public record TokenUsage(int inputTokens, int outputTokens, int 
totalTokens) {
+        public static final TokenUsage EMPTY = new TokenUsage(0, 0, 0);
+
+        public TokenUsage add(TokenUsage other) {
+            return new TokenUsage(
+                    inputTokens + other.inputTokens,
+                    outputTokens + other.outputTokens,
+                    totalTokens + other.totalTokens);
+        }
+    }
+
+    public record ChatResponse(String text, List<ToolCall> toolCalls, String 
stopReason, boolean streamed,
+            TokenUsage usage) {
+    }
+
+    public static String formatTokens(int tokens) {
+        if (tokens >= 1000) {
+            double k = tokens / 1000.0;
+            if (k == (int) k) {
+                return (int) k + "k";
+            }
+            return String.format("%.1fk", k);
+        }
+        return String.valueOf(tokens);
     }
 
     // -- Configuration --
@@ -388,12 +411,13 @@ public class LlmClient {
             if (response.statusCode() != 200) {
                 String errorBody = 
response.body().collect(Collectors.joining("\n"));
                 handleErrorStatus(response.statusCode(), errorBody);
-                return new ChatResponse(null, List.of(), "error", false);
+                return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
             }
 
             StringBuilder fullText = new StringBuilder();
             List<ToolCall> toolCalls = new ArrayList<>();
             String[] doneReasonHolder = { null };
+            int[] tokenHolder = { 0, 0 };
 
             response.body().forEach(line -> {
                 if (line.isBlank()) {
@@ -443,6 +467,8 @@ public class LlmClient {
 
                     if (Boolean.TRUE.equals(chunk.get("done"))) {
                         doneReasonHolder[0] = chunk.getString("done_reason");
+                        tokenHolder[0] = getIntValue(chunk, 
"prompt_eval_count");
+                        tokenHolder[1] = getIntValue(chunk, "eval_count");
                     }
                 } catch (Exception e) {
                     // skip malformed chunks
@@ -457,17 +483,19 @@ public class LlmClient {
             String stopReason
                     = !toolCalls.isEmpty() ? "tool_calls" : 
(doneReasonHolder[0] != null ? doneReasonHolder[0] : "stop");
 
+            TokenUsage usage = new TokenUsage(tokenHolder[0], tokenHolder[1], 
tokenHolder[0] + tokenHolder[1]);
             if (verbose) {
                 printer.println("[verbose] Streamed Ollama: text=" + (text != 
null ? truncateVerbose(text) : "null")
-                                + ", toolCalls=" + toolCalls.size() + ", 
doneReason=" + doneReasonHolder[0]);
+                                + ", toolCalls=" + toolCalls.size() + ", 
doneReason=" + doneReasonHolder[0]
+                                + ", tokens=" + usage.totalTokens());
             }
-            return new ChatResponse(text, toolCalls, stopReason, true);
+            return new ChatResponse(text, toolCalls, stopReason, true, usage);
         } catch (HttpTimeoutException e) {
             printer.println("\nRequest timed out after " + timeout + " 
seconds.");
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
         } catch (Exception e) {
             printer.println("\nError during streaming: " + e.getMessage());
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
         }
     }
 
@@ -699,20 +727,21 @@ public class LlmClient {
             if (verbose) {
                 printer.println("[verbose] parseOpenAiChatResponse: response 
is null");
             }
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
         }
+        TokenUsage usage = extractOpenAiUsage(response);
         JsonArray choices = (JsonArray) response.get("choices");
         if (choices == null || choices.isEmpty()) {
             if (verbose) {
                 printer.println("[verbose] parseOpenAiChatResponse: no choices 
in response. Keys: " + response.keySet());
             }
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, usage);
         }
         JsonObject firstChoice = (JsonObject) choices.get(0);
         String finishReason = firstChoice.getString("finish_reason");
         JsonObject message = (JsonObject) firstChoice.get("message");
         if (message == null) {
-            return new ChatResponse(null, List.of(), finishReason, false);
+            return new ChatResponse(null, List.of(), finishReason, false, 
usage);
         }
 
         String content = message.getString("content");
@@ -746,7 +775,7 @@ public class LlmClient {
             printer.println("[verbose] Parsed: text=" + (content != null ? 
truncateVerbose(content) : "null")
                             + ", toolCalls=" + toolCalls.size() + ", 
finishReason=" + finishReason);
         }
-        return new ChatResponse(content, toolCalls, finishReason, false);
+        return new ChatResponse(content, toolCalls, finishReason, false, 
usage);
     }
 
     private ChatResponse parseOllamaChatResponse(JsonObject response) {
@@ -754,14 +783,14 @@ public class LlmClient {
             if (verbose) {
                 printer.println("[verbose] parseOllamaChatResponse: response 
is null");
             }
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
         }
         JsonObject message = (JsonObject) response.get("message");
         if (message == null) {
             if (verbose) {
                 printer.println("[verbose] parseOllamaChatResponse: no message 
in response. Keys: " + response.keySet());
             }
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
         }
 
         String content = message.getString("content");
@@ -796,21 +825,27 @@ public class LlmClient {
 
         String stopReason = !toolCalls.isEmpty() ? "tool_calls" : (doneReason 
!= null ? doneReason : "stop");
 
+        int inputTokens = getIntValue(response, "prompt_eval_count");
+        int outputTokens = getIntValue(response, "eval_count");
+        TokenUsage usage = new TokenUsage(inputTokens, outputTokens, 
inputTokens + outputTokens);
+
         if (verbose) {
             printer.println("[verbose] Parsed Ollama: text=" + (content != 
null ? truncateVerbose(content) : "null")
-                            + ", toolCalls=" + toolCalls.size() + ", 
doneReason=" + doneReason);
+                            + ", toolCalls=" + toolCalls.size() + ", 
doneReason=" + doneReason
+                            + ", tokens=" + usage.totalTokens());
         }
-        return new ChatResponse(content, toolCalls, stopReason, false);
+        return new ChatResponse(content, toolCalls, stopReason, false, usage);
     }
 
     private ChatResponse parseAnthropicChatResponse(JsonObject response) {
         if (response == null) {
-            return new ChatResponse(null, List.of(), "error", false);
+            return new ChatResponse(null, List.of(), "error", false, 
TokenUsage.EMPTY);
         }
         String stopReason = response.getString("stop_reason");
+        TokenUsage usage = extractAnthropicUsage(response);
         JsonArray contentBlocks = (JsonArray) response.get("content");
         if (contentBlocks == null) {
-            return new ChatResponse(null, List.of(), stopReason, false);
+            return new ChatResponse(null, List.of(), stopReason, false, usage);
         }
 
         StringBuilder text = new StringBuilder();
@@ -828,7 +863,41 @@ public class LlmClient {
             }
         }
         String textContent = text.length() > 0 ? text.toString() : null;
-        return new ChatResponse(textContent, toolCalls, stopReason, false);
+        return new ChatResponse(textContent, toolCalls, stopReason, false, 
usage);
+    }
+
+    // ---- Token usage extraction ----
+
+    private TokenUsage extractOpenAiUsage(JsonObject response) {
+        JsonObject usage = (JsonObject) response.get("usage");
+        if (usage == null) {
+            return TokenUsage.EMPTY;
+        }
+        int prompt = getIntValue(usage, "prompt_tokens");
+        int completion = getIntValue(usage, "completion_tokens");
+        int total = getIntValue(usage, "total_tokens");
+        if (total == 0) {
+            total = prompt + completion;
+        }
+        return new TokenUsage(prompt, completion, total);
+    }
+
+    private TokenUsage extractAnthropicUsage(JsonObject response) {
+        JsonObject usage = (JsonObject) response.get("usage");
+        if (usage == null) {
+            return TokenUsage.EMPTY;
+        }
+        int input = getIntValue(usage, "input_tokens");
+        int output = getIntValue(usage, "output_tokens");
+        return new TokenUsage(input, output, input + output);
+    }
+
+    private static int getIntValue(JsonObject obj, String key) {
+        Object val = obj.get(key);
+        if (val instanceof Number n) {
+            return n.intValue();
+        }
+        return 0;
     }
 
     private String extractOpenAiContent(JsonObject response) {
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 d90a0efa92eb..cce76250427d 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
@@ -212,12 +212,14 @@ class ActionsPopup {
     }
 
     void setMcpEnabled(
-            boolean enabled, int port, Supplier<String> connectedClient, 
Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
+            boolean enabled, int port, Supplier<String> connectedClient,
+            Supplier<List<TuiMcpServer.LogEntry>> activityLog, 
Supplier<Integer> toolCallCount) {
         this.mcpEnabled = enabled;
         this.mcpPort = port;
         this.mcpConnectedClient = connectedClient;
         this.mcpActivityLog = activityLog;
         mcpLogPopup.setActivityLog(activityLog);
+        mcpLogPopup.setToolCallCount(toolCallCount);
     }
 
     void setAiActivityLog(Supplier<List<AiPanel.LogEntry>> activityLog) {
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 f700cddaee5b..ee4bb5c59e59 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
@@ -86,13 +86,14 @@ class AiPanel {
     private volatile Thread agentThread;
     private String initError;
     private long thinkingStartTime;
+    private volatile int sessionTotalTokens;
 
     // Activity log for AI Log popup
     private final List<LogEntry> activityLog = new ArrayList<>();
 
-    record ConversationEntry(String role, String text, long elapsedSeconds) {
+    record ConversationEntry(String role, String text, long elapsedSeconds, 
int totalTokens) {
         ConversationEntry(String role, String text) {
-            this(role, text, -1);
+            this(role, text, -1, 0);
         }
     }
 
@@ -127,6 +128,14 @@ class AiPanel {
         return "assistant".equals(last.role()) ? last.elapsedSeconds() : -1;
     }
 
+    private int lastResponseTokens() {
+        if (thinking.get() || conversation.isEmpty()) {
+            return 0;
+        }
+        ConversationEntry last = conversation.get(conversation.size() - 1);
+        return "assistant".equals(last.role()) ? last.totalTokens() : 0;
+    }
+
     void cycleHeight() {
         splitIndex = (splitIndex + 1) % SPLIT_PERCENTS.length;
     }
@@ -285,6 +294,7 @@ class AiPanel {
         }
         messages.add(LlmClient.Message.user(question));
 
+        LlmClient.TokenUsage totalUsage = LlmClient.TokenUsage.EMPTY;
         for (int i = 0; i < MAX_ITERATIONS; i++) {
             if (Thread.interrupted()) {
                 throw new InterruptedException();
@@ -297,6 +307,7 @@ class AiPanel {
                 log(LogLevel.ERROR, "Error", err);
                 return;
             }
+            totalUsage = totalUsage.add(response.usage());
 
             // check for error response (null text, no tool calls, error stop 
reason)
             if ("error".equals(response.stopReason())
@@ -324,10 +335,14 @@ class AiPanel {
                 messages.add(LlmClient.Message.toolResults(results));
             } else {
                 String text = response.text();
+                sessionTotalTokens += totalUsage.totalTokens();
                 if (text != null && !text.isBlank()) {
                     long elapsed = (System.currentTimeMillis() - 
thinkingStartTime) / 1000;
-                    conversation.add(new ConversationEntry("assistant", text, 
elapsed));
-                    log(LogLevel.RESPONSE, "Response (" + elapsed + "s)", 
text);
+                    conversation.add(new ConversationEntry("assistant", text, 
elapsed, totalUsage.totalTokens()));
+                    String tokenInfo = totalUsage.totalTokens() > 0
+                            ? ", " + 
LlmClient.formatTokens(totalUsage.totalTokens()) + " tokens"
+                            : "";
+                    log(LogLevel.RESPONSE, "Response (" + elapsed + "s" + 
tokenInfo + ")", text);
                 } else {
                     String err = "Empty response from LLM.";
                     conversation.add(new ConversationEntry("error", err));
@@ -344,13 +359,19 @@ class AiPanel {
     }
 
     void render(Frame frame, Rect area) {
-        // At 25% show elapsed in the title bar to save space
+        // At 25% show elapsed and tokens in the title bar to save space
         long titleElapsed = lastResponseElapsed();
+        int titleTokens = lastResponseTokens();
         Line titleLine;
         if (splitIndex == 0 && titleElapsed >= 0) {
+            String tokenSuffix = titleTokens > 0 ? ", " + 
LlmClient.formatTokens(titleTokens) + " tokens" : "";
+            titleLine = Line.from(
+                    Span.styled(" AI ", Style.EMPTY.bold()),
+                    Span.styled("(" + titleElapsed + "s" + tokenSuffix + ") ", 
Style.EMPTY.dim()));
+        } else if (sessionTotalTokens > 0) {
             titleLine = Line.from(
                     Span.styled(" AI ", Style.EMPTY.bold()),
-                    Span.styled("(" + titleElapsed + "s) ", 
Style.EMPTY.dim()));
+                    Span.styled("(total: " + 
LlmClient.formatTokens(sessionTotalTokens) + " tokens) ", Style.EMPTY.dim()));
         } else {
             titleLine = Line.from(Span.styled(" AI ", Style.EMPTY.bold()));
         }
@@ -419,12 +440,14 @@ class AiPanel {
             md.append(".".repeat((int) dots + 1)).append("*\n");
         }
 
-        // Show elapsed time as a dimmed line below the markdown when at the 
bottom
+        // Show elapsed time and token count as a dimmed line below the 
markdown when at the bottom
         long lastElapsed = -1;
+        int lastTokens = 0;
         if (!thinking.get() && !conversation.isEmpty()) {
             ConversationEntry last = conversation.get(conversation.size() - 1);
             if ("assistant".equals(last.role()) && last.elapsedSeconds() >= 0) 
{
                 lastElapsed = last.elapsedSeconds();
+                lastTokens = last.totalTokens();
             }
         }
 
@@ -478,8 +501,9 @@ class AiPanel {
         }
 
         if (elapsedArea != null && lastElapsed >= 0) {
+            String tokenSuffix = lastTokens > 0 ? ", " + 
LlmClient.formatTokens(lastTokens) + " tokens" : "";
             frame.renderWidget(
-                    Paragraph.from(Line.from(Span.styled("(" + lastElapsed + 
"s)", Style.EMPTY.dim()))),
+                    Paragraph.from(Line.from(Span.styled("(" + lastElapsed + 
"s" + tokenSuffix + ")", Style.EMPTY.dim()))),
                     elapsedArea);
         }
     }
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 4a49103dde62..37d15040f9c9 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
@@ -395,7 +395,8 @@ public class CamelMonitor extends CamelCommand {
             mcpServer = new TuiMcpServer(mcpPort, mcpFacade);
             try {
                 mcpServer.start();
-                actionsPopup.setMcpEnabled(true, mcpPort, 
mcpServer::getConnectedClient, mcpServer::getActivityLog);
+                actionsPopup.setMcpEnabled(true, mcpPort, 
mcpServer::getConnectedClient,
+                        mcpServer::getActivityLog, 
mcpServer::getToolCallCount);
                 mcpJsonFile = writeMcpJson(mcpPort);
             } catch (java.net.BindException e) {
                 System.err.println("MCP server failed to start: port " + 
mcpPort + " is already in use.");
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
index 94b69f55cd37..dea6a75d2be9 100644
--- 
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
@@ -48,6 +48,7 @@ class McpLogPopup {
 
     private boolean visible;
     private Supplier<List<TuiMcpServer.LogEntry>> activityLog;
+    private Supplier<Integer> toolCallCount;
     private List<TuiMcpServer.LogEntry> entries;
     private int selected;
     private int detailScroll;
@@ -56,6 +57,10 @@ class McpLogPopup {
         this.activityLog = activityLog;
     }
 
+    void setToolCallCount(Supplier<Integer> toolCallCount) {
+        this.toolCallCount = toolCallCount;
+    }
+
     boolean isVisible() {
         return visible;
     }
@@ -148,6 +153,16 @@ class McpLogPopup {
                     Span.raw(entry.message()))));
         }
 
+        int count = toolCallCount != null ? toolCallCount.get() : 0;
+        Line titleLine;
+        if (count > 0) {
+            titleLine = Line.from(
+                    Span.styled(" MCP Log ", Style.EMPTY.bold()),
+                    Span.styled("(" + count + " calls) ", Style.EMPTY.dim()));
+        } else {
+            titleLine = Line.from(Span.styled(" MCP Log ", 
Style.EMPTY.bold()));
+        }
+
         ListState masterState = new ListState();
         masterState.select(selected);
         ListWidget list = ListWidget.builder()
@@ -157,7 +172,7 @@ class McpLogPopup {
                 .scrollMode(ScrollMode.AUTO_SCROLL)
                 .block(Block.builder()
                         .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .title(" MCP Log ")
+                        .title(Title.from(titleLine))
                         .build())
                 .build();
         frame.renderStatefulWidget(list, area, masterState);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index 86e560387641..e265561c0dbb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -27,6 +27,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpServer;
@@ -72,6 +73,7 @@ class TuiMcpServer {
     private volatile String clientName;
     private volatile long lastActivity;
     private volatile long lastToolCallTime;
+    private final AtomicInteger toolCallCount = new AtomicInteger();
     private final List<LogEntry> activityLog = new ArrayList<>();
 
     TuiMcpServer(int port, McpFacade facade) {
@@ -119,6 +121,10 @@ class TuiMcpServer {
         return System.currentTimeMillis() - lastToolCallTime < 2000;
     }
 
+    int getToolCallCount() {
+        return toolCallCount.get();
+    }
+
     String getConnectedClient() {
         if (System.currentTimeMillis() - lastActivity < CLIENT_TIMEOUT_MS) {
             return clientName != null ? clientName : "unknown";
@@ -580,6 +586,7 @@ class TuiMcpServer {
         }
 
         lastToolCallTime = System.currentTimeMillis();
+        toolCallCount.incrementAndGet();
 
         String text;
         boolean isError = false;

Reply via email to