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 d9909a64d1c5 Camel TUI: add /context, /compact, /retry, /usage, /copy, 
/export and /prompt to the AI panel
d9909a64d1c5 is described below

commit d9909a64d1c530afb13b850529e42e996ae34da0
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 8 18:46:02 2026 +0200

    Camel TUI: add /context, /compact, /retry, /usage, /copy, /export and 
/prompt to the AI panel
    
    /context shows what the next request costs (provider and model, tool set,
    static prefix, history size, session total), /compact shrinks the history
    right away, /retry resends the last question from a clean turn, /prompt 
shows
    the system prompt, and /usage, /copy and /export are discoverable aliases 
for
    Ctrl+U, Ctrl+Y and Ctrl+E. /retry and /compact wait while a response is in
    progress, like /provider and /model.
    
    Camel TUI: /usage prints the AI usage summary in the chat instead of 
toggling the view
    
    A slash command belongs in the chat, and Ctrl+U already opens the full view.
    /usage now prints requests, tokens in and out, average latency, one line per
    model (and per route for observed GenAI spans) and the last request.
    
    Camel TUI: guard the AI panel prompt size with a budget test and add an 
opt-in Ollama benchmark
    
    AiPanelPromptBudgetTest builds the real system prompt and tool schemas and
    fails when the core set exceeds ~3.5k or the full set ~7.5k estimated 
tokens,
    so a regression in prompt size (which is latency for local models) is caught
    in CI without any model.
    
    AiPanelOllamaBenchmarkTest sends the same payload to a local Ollama and 
prints
    prompt processing, load, generation and wall time cold and warm in both 
modes.
    It is skipped unless CAMEL_TUI_OLLAMA_BENCH names the model to use.
    
    Camel TUI: README for the module with how to run the prompt budget and 
Ollama benchmark tests
    
    camel-jbang: pick the model from an OpenAI-compatible server and show the 
real error in the F8 panel
    
    With LLM_API_KEY and LLM_BASE_URL pointing at LM Studio, llama.cpp server,
    vLLM or similar, the client sent OpenAI's default model name (gpt-4o-mini, 
or
    the llama3.2 placeholder from camel ask), which such servers reject as not
    found. The F8 panel then only said 'LLM request failed. Check API key and
    endpoint.' because the HTTP status and the server's message went to stdout,
    which the TUI hides.
    
    Detection now asks a non-OpenAI endpoint for /v1/models and uses the first
    model it hosts unless one was configured, and the panel collects what the
    client prints and shows it with the error. Docs explain how to pick another
    model and that it must support tool calling.
    
    Camel TUI: tui_get_table loads on-demand tabs instead of reporting no data
    
    The Classpath, Maven Dependencies, Catalog, CVE Audit and Startup tabs only
    fetch their data when the user opens them, so tui_get_table on a tab that 
was
    never opened returned 'No table data available' and the AI concluded the TUI
    could not answer (for example how many JARs are on the classpath). Add
    ensureDataLoaded/dataLoadError hooks to MonitorTab, implemented by those 
five
    tabs, and make the facade start the load and wait for the result (up to 8s,
    or until the load reports an error or an empty tab), returning that reason
    to the caller instead of the generic message.
    
    Camel TUI: end stuck tool loops in the AI panel with an explanation instead 
of the bare iteration limit
    
    A model that keeps calling the same tool with the same arguments (for 
example
    a failing tui_send_message to a route that only consumes from a broker) used
    to run until 'Reached maximum iterations (10)' with nothing else to go on.
    Now the third identical call is answered with a note telling the model to
    stop and report, the limit message lists the last tool calls and results and
    points at the AI log and /retry, and the history stays consistent so the 
next
    question starts fresh. The prompt and the tui_send_message description say
    that any endpoint URI works, so a broker-fed route is fed by publishing to 
the
    broker with the route's own component and options.
    
    Camel TUI: tui_send_message hints at the right component when the scheme is 
not a Camel component
    
    A model asked to feed the MQTT example sent to 
mqtt:temperature?brokerUrl=...,
    but the component is paho-mqtt5 (the mqtt5-source kamelet hides that from 
the
    route). When a send fails because the scheme is unknown, the tool now adds a
    hint naming similar catalog components (aliases such as mqtt -> paho-mqtt5,
    paho, rabbitmq -> spring-rabbitmq, s3 -> aws2-s3, then substring matches) 
and
    an example URI, so the next call uses the right scheme instead of guessing
    until the iteration limit.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-ai.adoc         |   3 +
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  25 ++
 .../camel/dsl/jbang/core/commands/LlmClient.java   |  31 +-
 .../core/commands/LlmClientListModelsTest.java     |  31 ++
 dsl/camel-jbang/camel-jbang-plugin-tui/README.md   |  83 +++++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 400 +++++++++++++++++++--
 .../core/commands/tui/AiSlashCommandContext.java   |  32 ++
 .../core/commands/tui/AiSlashCommandRegistry.java  |  31 ++
 .../dsl/jbang/core/commands/tui/CatalogTab.java    |  25 +-
 .../dsl/jbang/core/commands/tui/ClasspathTab.java  |  25 +-
 .../dsl/jbang/core/commands/tui/CveAuditTab.java   |  23 +-
 .../jbang/core/commands/tui/DocViewerPopup.java    |   4 +-
 .../core/commands/tui/MavenDependenciesTab.java    |  25 +-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  51 ++-
 .../dsl/jbang/core/commands/tui/MonitorTab.java    |  18 +
 .../dsl/jbang/core/commands/tui/StartupTab.java    |  23 +-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  85 ++++-
 .../commands/tui/AiPanelHistoryCompactionTest.java |  13 +
 .../commands/tui/AiPanelOllamaBenchmarkTest.java   | 166 +++++++++
 .../core/commands/tui/AiPanelPromptBudgetTest.java | 117 ++++++
 .../dsl/jbang/core/commands/tui/AiPanelTest.java   | 192 +++++++++-
 .../commands/tui/AiSlashCommandRegistryTest.java   |  74 +++-
 .../commands/tui/McpFacadeAwaitTableDataTest.java  | 128 +++++++
 .../tui/TuiToolRegistrySuggestComponentsTest.java  |  46 +++
 24 files changed, 1579 insertions(+), 72 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
index 74f11eecd891..8c63c22e731f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
@@ -192,6 +192,9 @@ camel ask "what routes are running?"
 
 `OPENAI_BASE_URL` is accepted as an alternative to `LLM_BASE_URL` (common in 
other tools).
 
+The first model the server lists on `/v1/models` is used unless `--model` 
names another one; the
+model must support tool calling.
+
 Common OpenAI-compatible servers:
 
 [options="header"]
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 7ab129ae3ad2..62e6ce0cd6b0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -842,6 +842,13 @@ camel tui
 
 `OPENAI_BASE_URL` is also accepted as an alternative to `LLM_BASE_URL`.
 
+The panel uses the first model the server lists on `/v1/models`. To use 
another one, run
+`/model <name>` in the panel (`/model` alone lists what the server offers), 
set *AI Model* in
+*F2 -> Settings*, or set `camel.tui.ai.model`. The model must support tool 
calling, otherwise the
+panel answers from training data instead of inspecting your integration. When 
a request fails, the
+panel shows the HTTP status and the server's error message, for example a 
model that the server
+does not host.
+
 === Why This Matters
 
 When an AI agent connects to the TUI via MCP, it gains the same level of 
visibility that you
@@ -920,6 +927,24 @@ cycles backward.
 | `/tools [auto\|core\|full]` (`/t`)
 | Show which tool set is sent to the model, or switch it. `auto` (default) 
sends the core set to local providers and every tool to hosted ones; the choice 
is saved as `camel.tui.ai.tools`.
 
+| `/context` (`/ctx`)
+| Show what the next request costs: provider and model, tool set, static 
prefix size, history size and the session total. Useful with local models, 
where prompt size is time.
+
+| `/compact`
+| Shrink the conversation history sent to the model right away: older tool 
results are cut to their first lines and the oldest turns are dropped. The 
panel does this automatically after each answer for all but the latest turn.
+
+| `/retry`
+| Send the last question again, starting from a clean turn in the model 
history.
+
+| `/usage` (`/u`)
+| Print the AI usage so far in the chat: requests, tokens in and out, average 
latency, one line per model (and per route when GenAI spans are observed), and 
the last request. *Ctrl+U* opens the full view with the per-turn chart.
+
+| `/copy` (`/y`), `/export` (`/e`)
+| The same as *Ctrl+Y* (copy the last response) and *Ctrl+E* (export the 
conversation to Markdown).
+
+| `/prompt`
+| Show the system prompt the panel sends with every request.
+
 | `/clear` (`/c`)
 | Clear the AI conversation, usage counters, and model context without 
changing the provider or model.
 
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 d472c0511bb4..0907110de505 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
@@ -335,8 +335,8 @@ public class LlmClient {
                 if (openAiAuthMode == OpenAiAuthMode.api_key
                         || (url != null && isAzureOpenAiEndpoint(url))) {
                     resolveAzureOpenAiModel();
-                } else if (model == null || model.isBlank()) {
-                    model = DEFAULT_OPENAI_MODEL;
+                } else if (model == null || model.isBlank() || 
DEFAULT_OLLAMA_MODEL.equals(model)) {
+                    model = isOpenAiCompatibleServer() ? 
resolveOpenAiCompatibleModel() : DEFAULT_OPENAI_MODEL;
                 }
             }
             case gemini -> {
@@ -1989,6 +1989,33 @@ public class LlmClient {
                 .orElse(available.get(0));
     }
 
+    /**
+     * Whether the OpenAI-style endpoint is something other than OpenAI itself 
(LM Studio, vLLM, llama.cpp server,
+     * LocalAI and friends reached through {@code LLM_BASE_URL} / {@code 
OPENAI_BASE_URL}). Those servers only know the
+     * models they host, so OpenAI's default model name is rejected there.
+     */
+    private boolean isOpenAiCompatibleServer() {
+        return url != null && !url.contains("api.openai.com");
+    }
+
+    /**
+     * Picks the first model an OpenAI-compatible server reports on {@code 
/v1/models}, since a hard-coded OpenAI model
+     * name would be rejected with "model not found". Falls back to the OpenAI 
default when the list is empty or the
+     * endpoint does not implement it.
+     */
+    private String resolveOpenAiCompatibleModel() {
+        try {
+            List<String> available = listOpenAiModels();
+            if (!available.isEmpty()) {
+                printer.println("Auto-selected model: " + available.get(0) + " 
(first model reported by " + url + ")");
+                return available.get(0);
+            }
+        } catch (Exception e) {
+            // best-effort, keep default
+        }
+        return DEFAULT_OPENAI_MODEL;
+    }
+
     private void resolveOllamaModel() {
         try {
             HttpRequest request = HttpRequest.newBuilder()
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientListModelsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientListModelsTest.java
index 3b9b45a1a17a..a90baba72d04 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientListModelsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientListModelsTest.java
@@ -171,6 +171,37 @@ class LlmClientListModelsTest {
         assertEquals("gpt-4o-mini", client.model(), "OpenAI detection must 
leave a usable default model, not null");
     }
 
+    @Test
+    void picksFirstModelOfAnOpenAiCompatibleServerWhenNoneConfigured() throws 
IOException {
+        // LM Studio, llama.cpp server and friends reject OpenAI's default 
model name, so the first hosted model wins
+        String baseUrl = startServer("/v1/models", 200,
+                
"{\"data\":[{\"id\":\"qwen3.6-35b-a3b\"},{\"id\":\"gemma-4-12b\"}]}", null, 
null);
+        LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.openai).withUrl(baseUrl);
+
+        assertTrue(client.detectEndpoint());
+        assertEquals("qwen3.6-35b-a3b", client.model());
+    }
+
+    @Test
+    void replacesTheCliPlaceholderModelOnAnOpenAiCompatibleServer() throws 
IOException {
+        String baseUrl = startServer("/v1/models", 200, 
"{\"data\":[{\"id\":\"local-llm\"}]}", null, null);
+        LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.openai).withUrl(baseUrl)
+                .withModel("llama3.2");
+
+        assertTrue(client.detectEndpoint());
+        assertEquals("local-llm", client.model(), "the llama3.2 placeholder 
the CLI passes must not reach the server");
+    }
+
+    @Test
+    void keepsAnExplicitModelOnAnOpenAiCompatibleServer() throws IOException {
+        String baseUrl = startServer("/v1/models", 200, 
"{\"data\":[{\"id\":\"local-llm\"}]}", null, null);
+        LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.openai).withUrl(baseUrl)
+                .withModel("my-tuned-model");
+
+        assertTrue(client.detectEndpoint());
+        assertEquals("my-tuned-model", client.model());
+    }
+
     @Test
     void fallsBackToDefaultOllamaModelWhenModelsCannotBeListed() throws 
IOException {
         // Root responds so the endpoint is detected, but /api/tags is 
unavailable, so the installed models cannot be
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/README.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/README.md
new file mode 100644
index 000000000000..2d357e021f5f
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/README.md
@@ -0,0 +1,83 @@
+# Camel CLI TUI plugin
+
+This module provides `camel tui`, the terminal dashboard for running Camel 
integrations, including the
+F8 AI panel and the embedded MCP server (`camel tui --mcp`).
+
+User documentation lives in the user manual: 
`docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc`.
+
+## Building
+
+```bash
+mvn install -DskipTests
+```
+
+installs the plugin JAR into the local Maven repository, which is what a 
locally built `camel` CLI picks up.
+Restart `camel tui` afterwards.
+
+## AI panel prompt size and local model performance
+
+The F8 panel sends a static prefix (system prompt plus the schemas of the 
`tui_*` tools) with every request.
+A local model pays for every token of it in prompt-processing time on every 
question, so its size is
+guarded and measured by two tests in 
`src/test/java/org/apache/camel/dsl/jbang/core/commands/tui`.
+
+### `AiPanelPromptBudgetTest` (always runs)
+
+Builds the real system prompt and tool schemas for the `core` set (sent to 
local providers) and the
+`full` set (sent to hosted providers) and fails when they exceed a budget of 
estimated tokens. It runs in
+milliseconds, needs no model, and prints the breakdown:
+
+```bash
+mvn test -Dtest=AiPanelPromptBudgetTest
+```
+
+If a change genuinely needs more room, raise the budget in the same commit and 
explain why in the message.
+
+### `AiPanelOllamaBenchmarkTest` (opt-in)
+
+Sends the same payload to a local Ollama and prints how long the model spends 
on prompt processing and
+generation, cold and warm, in both tool modes. It is skipped unless 
`CAMEL_TUI_OLLAMA_BENCH` names the
+model to use.
+
+Prerequisites:
+
+1. Ollama installed natively (`brew install ollama` on macOS) and running: 
`ollama serve`
+2. The model pulled, for example the recommended one for the TUI: `ollama pull 
qwen3.6:35b-a3b`
+
+Run:
+
+```bash
+CAMEL_TUI_OLLAMA_BENCH=qwen3.6:35b-a3b mvn test 
-Dtest=AiPanelOllamaBenchmarkTest
+```
+
+`OLLAMA_HOST` overrides the server URL (default `http://localhost:11434`). The 
output looks like this
+(Apple M4 Pro, 64 GB):
+
+```
+AI panel benchmark against http://localhost:11434 with qwen3.6:35b-a3b
+== full: 47 tools ==
+  1st (cold prefix)        prompt= 7283 tok  prompt_eval= 10.4s (  701 tok/s)  
load= 0.0s  gen= 16 tok in  0.2s  wall= 10.7s  tool=tui_get_state
+  2nd (warm prefix)        prompt= 7284 tok  prompt_eval=  1.6s ( 4455 tok/s)  
load= 0.0s  gen= 28 tok in  0.4s  wall=  2.1s  tool=tui_get_table
+  3rd (switched integr.)   prompt= 7282 tok  prompt_eval=  1.6s ( 4467 tok/s)  
load= 0.0s  gen= 16 tok in  0.3s  wall=  1.9s  tool=tui_get_errors
+== core: 19 tools ==
+  1st (cold prefix)        prompt= 3222 tok  prompt_eval=  4.7s (  684 tok/s)  
load= 0.0s  gen= 26 tok in  0.4s  wall=  5.2s  tool=tui_get_state
+  2nd (warm prefix)        prompt= 3223 tok  prompt_eval=  1.5s ( 2097 tok/s)  
load= 0.0s  gen= 30 tok in  0.5s  wall=  2.0s  tool=tui_get_state
+  3rd (switched integr.)   prompt= 3221 tok  prompt_eval=  1.5s ( 2104 tok/s)  
load= 0.0s  gen= 16 tok in  0.3s  wall=  1.8s  tool=tui_get_errors
+```
+
+How to read it:
+
+- `prompt_eval` on the 1st request is the cost of processing the whole prefix; 
on the 2nd and 3rd it
+  should drop to the new tokens only, which shows the Ollama prompt cache is 
being reused. If the 2nd
+  request costs as much as the 1st, something in the prefix changes between 
requests, or the engine
+  cannot reuse the cache (the Ollama MLX engine cannot for Qwen 3.x models; 
use the default GGUF tags).
+- The 3rd request switches the monitored integration; it must stay warm 
because that information travels
+  in the user message, not in the system prompt.
+- `tool=` shows which tool the model chose for the question, a quick sanity 
check that trimmed
+  descriptions did not hurt tool selection (`tui_get_state` for "what model is 
this",
+  `tui_get_table` for "what routes are running?", `tui_get_errors` for "any 
errors?").
+- Dense models (27B, 32B) process prompts at roughly 110 tokens per second on 
an M4 Pro, so the 1st
+  request takes about a minute. Mixture-of-experts models such as 
`qwen3.6:35b-a3b` are several times
+  faster, which is why the TUI recommends them.
+
+The same figures are available live inside the TUI: `/context` in the F8 panel 
shows the prefix and
+history size of the next request, and `/usage` the tokens and latency spent so 
far.
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 4e6ecec0beb4..e1df0d622eee 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
@@ -23,7 +23,10 @@ import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -63,6 +66,7 @@ import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 import org.apache.camel.dsl.jbang.core.commands.LlmClient;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.util.json.JsonObject;
 
 /**
@@ -72,6 +76,11 @@ import org.apache.camel.util.json.JsonObject;
 class AiPanel {
 
     private static final int MAX_ITERATIONS = 10;
+    /**
+     * A tool call repeated this many times with identical arguments in one 
turn is not executed again; the model gets a
+     * note instead, so a stuck loop ends with an explanation rather than at 
the iteration limit.
+     */
+    static final int MAX_IDENTICAL_TOOL_CALLS = 3;
     /**
      * Longest tool result handed to the model. The AI log keeps the full 
text; the model gets the head plus a note,
      * because a single log or table dump can otherwise be larger than the 
whole system prompt.
@@ -158,6 +167,37 @@ class AiPanel {
     // Slash commands
     private final AiSlashCommandRegistry slashCommands = 
AiSlashCommandRegistry.defaults();
     private AiSlashCommandContext slashCommandContext = new 
PanelSlashCommandContext();
+    // Lines the LLM client prints while detecting the endpoint or failing a 
request (HTTP status, provider error
+    // message, auto-selected model). The TUI hides stdout, so they are 
collected here and shown with the error.
+    private final Deque<String> clientOutput = new ArrayDeque<>();
+    private final Printer clientPrinter = new Printer() {
+        @Override
+        public void println() {
+        }
+
+        @Override
+        public void println(String line) {
+            print(line);
+        }
+
+        @Override
+        public void print(String output) {
+            if (output == null || output.isBlank()) {
+                return;
+            }
+            synchronized (clientOutput) {
+                clientOutput.addLast(output.strip());
+                while (clientOutput.size() > 6) {
+                    clientOutput.removeFirst();
+                }
+            }
+        }
+
+        @Override
+        public void printf(String format, Object... args) {
+            print(String.format(format, args));
+        }
+    };
     // auto | core | full, see useCoreTools(); loaded from camel.tui.ai.tools, 
null means auto
     private volatile String toolMode;
     private final AiCliCommandExecutor cliCommandExecutor = new 
AiCliCommandExecutor();
@@ -327,7 +367,8 @@ class AiPanel {
             LlmClient created = LlmClient.create()
                     .withTemperature(0.3)
                     .withTimeout(120)
-                    .withMaxTokens(4096);
+                    .withMaxTokens(4096)
+                    .withPrinter(clientPrinter);
             if (sessionProviderChoice != null) {
                 providerSelector.applyChoice(created, 
sessionProviderChoice.provider(), sessionProviderChoice.model(),
                         sessionProviderChoice.url());
@@ -451,11 +492,7 @@ class AiPanel {
             return true;
         }
         if (ke.hasCtrl() && ke.isCharIgnoreCase('u')) {
-            statsView = !statsView;
-            statsScrollOffset = 0;
-            if (statsView) {
-                spanRefreshRequested = true;
-            }
+            toggleUsageView();
             return true;
         }
         if (ke.hasCtrl() && ke.isCharIgnoreCase('y')) {
@@ -854,10 +891,11 @@ class AiPanel {
         Optional<AiSlashCommandRegistry.ParsedCommand> parsed = 
slashCommands.parse(input);
         if (parsed.isPresent() && (thinking.get() || activeCliCommand != 
null)) {
             String name = parsed.get().descriptor().name();
-            if ("provider".equals(name) || "model".equals(name)) {
+            if ("provider".equals(name) || "model".equals(name) || 
"retry".equals(name)
+                    || "compact".equals(name)) {
                 conversation.add(new ConversationEntry(
                         AiRole.SYSTEM,
-                        "Wait for the current operation to finish before 
changing provider or model."));
+                        "Wait for the current operation to finish before 
running /" + name + "."));
                 return;
             }
         }
@@ -1012,12 +1050,15 @@ class AiPanel {
         messages.add(LlmClient.Message.user(contextualize(question)));
 
         LlmClient.TokenUsage totalUsage = LlmClient.TokenUsage.EMPTY;
+        Map<String, Integer> callCounts = new HashMap<>();
+        List<String> recentCalls = new ArrayList<>();
         for (int i = 0; i < MAX_ITERATIONS; i++) {
             if (Thread.interrupted()) {
                 throw new InterruptedException();
             }
 
             long callStart = System.currentTimeMillis();
+            drainClientOutput();
             LlmClient.ChatResponse response = 
client.chatWithTools(systemPrompt, messages, tools);
             long callLatency = System.currentTimeMillis() - callStart;
             if (response == null) {
@@ -1033,7 +1074,11 @@ class AiPanel {
             if ("error".equals(response.stopReason())
                     && (response.toolCalls() == null || 
response.toolCalls().isEmpty())
                     && response.text() == null) {
-                String err = "LLM request failed. Check API key and endpoint.";
+                String detail = drainClientOutput();
+                String err = detail.isEmpty()
+                        ? "LLM request failed. Check API key and endpoint."
+                        : "LLM request failed: " + detail
+                          + "\nCheck the endpoint and model (/model lists what 
the provider offers).";
                 conversation.add(new ConversationEntry(AiRole.ERROR, err));
                 log(LogLevel.ERROR, "Error", err);
                 return;
@@ -1047,9 +1092,20 @@ class AiPanel {
                     if (Thread.interrupted()) {
                         throw new InterruptedException();
                     }
-                    log(LogLevel.TOOL, toolCall.name(), 
toolCall.arguments().toJson());
-                    String result = executeTuiTool(toolCall.name(), 
toolCall.arguments());
+                    String arguments = toolCall.arguments() != null ? 
toolCall.arguments().toJson() : "{}";
+                    log(LogLevel.TOOL, toolCall.name(), arguments);
+                    String key = toolCall.name() + " " + arguments;
+                    int repeats = callCounts.merge(key, 1, Integer::sum);
+                    String result;
+                    if (repeats > MAX_IDENTICAL_TOOL_CALLS) {
+                        result = "You have already called " + toolCall.name() 
+ " with these exact arguments "
+                                 + (repeats - 1) + " times in this turn and 
the result will not change. Stop calling "
+                                 + "tools now: tell the user what you found, 
what failed, and what they could try instead.";
+                    } else {
+                        result = executeTuiTool(toolCall.name(), 
toolCall.arguments());
+                    }
                     log(LogLevel.RESULT, toolCall.name(), result);
+                    recentCalls.add(toolCall.name() + " " + 
summarize(arguments, 80) + " -> " + summarize(result, 120));
                     results.add(new LlmClient.ToolResult(toolCall.id(), 
truncateToolResult(result)));
                 }
                 messages.add(LlmClient.Message.toolResults(results));
@@ -1074,9 +1130,30 @@ class AiPanel {
                 return;
             }
         }
-        conversation.add(new ConversationEntry(
-                AiRole.ERROR,
-                "Reached maximum iterations (" + MAX_ITERATIONS + ") without a 
final answer."));
+        StringBuilder sb = new StringBuilder();
+        sb.append("Reached maximum iterations 
(").append(MAX_ITERATIONS).append(") without a final answer. ");
+        sb.append("The model kept calling tools instead of answering; the last 
calls were:");
+        int from = Math.max(0, recentCalls.size() - 4);
+        for (String call : recentCalls.subList(from, recentCalls.size())) {
+            sb.append("\n- ").append(call);
+        }
+        sb.append("\nSee F2 -> AI Log for the full results, then rephrase with 
more detail (for example the exact ");
+        sb.append("endpoint URI or topic) or /retry.");
+        sessionTotalTokens += totalUsage.totalTokens();
+        conversation.add(new ConversationEntry(AiRole.ERROR, sb.toString()));
+        log(LogLevel.ERROR, "Error", sb.toString());
+        // keep the history consistent: the turn ends without an answer, so 
the next question starts fresh from here
+        messages.add(LlmClient.Message.assistantWithToolCalls(
+                "(no answer: the iteration limit was reached while calling 
tools)", List.of()));
+        compactHistory(messages, MAX_HISTORY_TURNS, COMPACT_TOOL_RESULT_CHARS);
+    }
+
+    private static String summarize(String text, int max) {
+        if (text == null) {
+            return "";
+        }
+        String flat = text.replace('\n', ' ').replace('\r', ' ').strip();
+        return flat.length() <= max ? flat : flat.substring(0, max) + "...";
     }
 
     private void recordUsage(LlmClient.ChatResponse response, long latencyMs) {
@@ -1797,20 +1874,21 @@ class AiPanel {
         sb.append("You have tui_* tools to observe and interact with the TUI; 
the tool definitions describe each one. ");
         sb.append("All tui_get_* tools fetch data directly from any tab 
without changing what the user sees.\n\n");
         sb.append("Guidelines:\n");
-        sb.append("- NEVER call tui_navigate just to read data - use the 
tui_get_* tools instead\n");
-        sb.append("- Prefer tui_get_table over tui_get_screen for structured 
data ");
-        sb.append("- it fetches from any tab using the tab parameter, no 
navigation needed\n");
-        sb.append("- Call tui_get_options only when unsure which tab holds the 
data you need\n");
-        sb.append("- Use tui_get_state to learn which integration and tab is 
selected, if the question depends on it\n");
-        sb.append("- Use tui_get_processor_detail to explain what a route does 
or how its steps are configured\n");
-        sb.append("- Use tui_get_status for data no tab shows (context, 
runtime, health checks, properties, ...)\n");
+        sb.append("- NEVER call tui_navigate just to read data; the tui_get_* 
tools read any tab without navigating\n");
+        sb.append("- Prefer tui_get_table over tui_get_screen for structured 
data; ");
+        sb.append("call tui_get_options only when unsure which tab holds the 
data\n");
+        sb.append("- tui_get_state tells which integration and tab is 
selected; tui_get_processor_detail explains ");
+        sb.append("a route's steps; tui_get_status has data no tab shows 
(context, runtime, health, properties)\n");
         sb.append("- Your own tool calls are recorded in the AI log 
(tui_get_ai_log, F2 -> AI Log); ");
         sb.append("the MCP log only records external clients\n");
-        sb.append("- Be concise and actionable in your answers\n");
-        sb.append("- When something looks wrong, explain what it means and 
suggest fixes\n");
-        sb.append("- For stopping routes or applications, use tui_control for 
graceful shutdown; ");
-        sb.append("tui_control reset-stats clears statistics without touching 
the routes\n");
+        sb.append("- Be concise and actionable; when something looks wrong, 
explain what it means and suggest fixes\n");
+        sb.append("- tui_control stops/starts routes and integrations 
gracefully; its reset-stats action clears ");
+        sb.append("statistics without touching the routes\n");
         sb.append("- Never restart, stop or kill an integration unless the 
user explicitly asked for that\n");
+        sb.append("- If a tool call returns an error, do not repeat it with 
the same arguments; ");
+        sb.append("tell the user what failed and what to try\n");
+        sb.append("- To feed a route that consumes from a broker (MQTT, Kafka, 
JMS), tui_send_message can publish ");
+        sb.append("to the broker with the route's own component and 
options\n");
         if (!useCoreTools()) {
             sb.append("- Use tui_locate + tui_draw_shape to visually highlight 
problems on screen for the user\n");
         }
@@ -1874,6 +1952,17 @@ class AiPanel {
         return defs;
     }
 
+    /**
+     * Returns and clears what the LLM client printed since the last drain, 
joined on one line.
+     */
+    private String drainClientOutput() {
+        synchronized (clientOutput) {
+            String joined = String.join(" | ", clientOutput);
+            clientOutput.clear();
+            return joined;
+        }
+    }
+
     /**
      * Caps a tool result before it enters the model history; the AI log keeps 
the full text.
      */
@@ -1893,6 +1982,16 @@ class AiPanel {
      * message through the final answer) so assistant tool calls never lose 
their matching results.
      */
     static void compactHistory(List<LlmClient.Message> history, int maxTurns, 
int compactChars) {
+        compactHistory(history, maxTurns, compactChars, true);
+    }
+
+    /**
+     * As {@link #compactHistory(List, int, int)}; with {@code 
keepPreviousTurn} false the most recent answered turn is
+     * compacted as well (used by {@code /compact}).
+     */
+    static void compactHistory(
+            List<LlmClient.Message> history, int maxTurns, int compactChars,
+            boolean keepPreviousTurn) {
         if (history == null || history.isEmpty()) {
             return;
         }
@@ -1910,11 +2009,13 @@ class AiPanel {
             userIndexes = userIndexes.subList(dropped, 
userIndexes.size()).stream()
                     .map(index -> index - keepFrom).toList();
         }
-        // everything before the previous turn (i.e. before the second-last 
user message) is compacted
-        if (userIndexes.size() < 2) {
+        // everything before the previous turn (i.e. before the second-last 
user message) is compacted; /compact
+        // also compacts the previous turn itself
+        int keep = keepPreviousTurn ? 2 : 1;
+        if (userIndexes.size() < keep) {
             return;
         }
-        int compactBefore = userIndexes.get(userIndexes.size() - 2);
+        int compactBefore = keepPreviousTurn ? 
userIndexes.get(userIndexes.size() - 2) : history.size();
         for (int i = 0; i < compactBefore; i++) {
             LlmClient.Message m = history.get(i);
             if (m.toolResults() == null || m.toolResults().isEmpty()) {
@@ -1937,6 +2038,214 @@ class AiPanel {
         }
     }
 
+    /**
+     * Rough token count for prompt text and JSON: about four characters per 
token for the mix of English and JSON the
+     * panel sends.
+     */
+    static int estimateTokens(long chars) {
+        return (int) ((chars + 3) / 4);
+    }
+
+    static long historyChars(List<LlmClient.Message> history) {
+        if (history == null) {
+            return 0;
+        }
+        long chars = 0;
+        for (LlmClient.Message m : history) {
+            if (m.content() != null) {
+                chars += m.content().length();
+            }
+            if (m.toolCalls() != null) {
+                for (LlmClient.ToolCall tc : m.toolCalls()) {
+                    chars += tc.name().length() + (tc.arguments() != null ? 
tc.arguments().toJson().length() : 0);
+                }
+            }
+            if (m.toolResults() != null) {
+                for (LlmClient.ToolResult tr : m.toolResults()) {
+                    chars += tr.content() != null ? tr.content().length() : 0;
+                }
+            }
+        }
+        return chars;
+    }
+
+    private static long toolResultChars(List<LlmClient.Message> history) {
+        long chars = 0;
+        if (history != null) {
+            for (LlmClient.Message m : history) {
+                if (m.toolResults() != null) {
+                    for (LlmClient.ToolResult tr : m.toolResults()) {
+                        chars += tr.content() != null ? tr.content().length() 
: 0;
+                    }
+                }
+            }
+        }
+        return chars;
+    }
+
+    private static int countTurns(List<LlmClient.Message> history) {
+        int turns = 0;
+        if (history != null) {
+            for (LlmClient.Message m : history) {
+                if ("user".equals(m.role()) && m.toolCalls() == null && 
m.toolResults() == null) {
+                    turns++;
+                }
+            }
+        }
+        return turns;
+    }
+
+    private long toolSchemaChars() {
+        long chars = 0;
+        for (LlmClient.ToolDef def : buildTuiToolDefinitions()) {
+            chars += 40 + def.name().length() + (def.description() != null ? 
def.description().length() : 0)
+                     + (def.parameters() != null ? 
def.parameters().toJson().length() : 0);
+        }
+        return chars;
+    }
+
+    String describeContext() {
+        StringBuilder sb = new StringBuilder();
+        if (client == null) {
+            sb.append("Provider: none (").append(initError != null ? initError 
: "no LLM client").append(")\n");
+        } else {
+            sb.append("Provider: ").append(client.apiType() != null ? 
client.apiType().name() : "unknown");
+            if (client.endpointUrl() != null) {
+                sb.append(" at ").append(client.endpointUrl());
+            }
+            sb.append(", model ").append(client.model() != null ? 
client.model() : "auto");
+            sb.append(client.isLocalEndpoint() ? " (local)" : " 
(hosted)").append('\n');
+        }
+        sb.append("Tools: ").append(describeToolMode()).append('\n');
+        int promptTokens = estimateTokens(buildSystemPrompt().length());
+        int toolTokens = estimateTokens(toolSchemaChars());
+        sb.append("Static prefix: 
~").append(LlmClient.formatTokens(promptTokens + toolTokens))
+                .append(" tokens (system prompt 
~").append(LlmClient.formatTokens(promptTokens))
+                .append(", tool schemas 
~").append(LlmClient.formatTokens(toolTokens)).append(")\n");
+        int historyTokens = estimateTokens(historyChars(messages));
+        int resultTokens = estimateTokens(toolResultChars(messages));
+        sb.append("History: ").append(countTurns(messages)).append(" turn(s), 
")
+                .append(messages != null ? messages.size() : 0).append(" 
message(s), ~")
+                .append(LlmClient.formatTokens(historyTokens)).append(" tokens 
(tool results ~")
+                .append(LlmClient.formatTokens(resultTokens)).append("); 
/compact shrinks it, /clear resets it\n");
+        sb.append("Next request: 
~").append(LlmClient.formatTokens(promptTokens + toolTokens + historyTokens))
+                .append(" tokens before your question; session total so far ")
+                .append(LlmClient.formatTokens(sessionTotalTokens)).append(" 
tokens");
+        return sb.toString();
+    }
+
+    String compactHistoryNow() {
+        if (messages == null || messages.isEmpty()) {
+            return "History is empty, nothing to compact";
+        }
+        int before = estimateTokens(historyChars(messages));
+        int messagesBefore = messages.size();
+        compactHistory(messages, MAX_HISTORY_TURNS, COMPACT_TOOL_RESULT_CHARS, 
false);
+        int after = estimateTokens(historyChars(messages));
+        return "Compacted history: " + messagesBefore + " -> " + 
messages.size() + " message(s), ~"
+               + LlmClient.formatTokens(before) + " -> ~" + 
LlmClient.formatTokens(after) + " tokens";
+    }
+
+    /**
+     * Resends the last question. Any messages from the previous attempt (the 
question and whatever followed it) are
+     * removed from the model history first so the retry starts from a clean 
turn.
+     */
+    boolean retryLastQuestion() {
+        if (client == null || thinking.get()) {
+            return false;
+        }
+        String question = null;
+        for (int i = conversation.size() - 1; i >= 0; i--) {
+            if (conversation.get(i).role() == AiRole.USER) {
+                question = conversation.get(i).text();
+                break;
+            }
+        }
+        if (question == null || question.isBlank()) {
+            return false;
+        }
+        if (messages != null) {
+            for (int i = messages.size() - 1; i >= 0; i--) {
+                LlmClient.Message m = messages.get(i);
+                if ("user".equals(m.role()) && m.toolCalls() == null && 
m.toolResults() == null) {
+                    messages.subList(i, messages.size()).clear();
+                    break;
+                }
+            }
+        }
+        submitQuestion(question);
+        return true;
+    }
+
+    /**
+     * The usage figures of the Ctrl+U view as text for the chat: totals, then 
one line per model (and per route for
+     * GenAI spans from the monitored integration).
+     */
+    String usageSummary() {
+        List<AiUsageEntry> entries = combinedUsageEntries();
+        if (entries.isEmpty()) {
+            return "No AI usage yet. Ask a question, or run routes with GenAI 
observability and --observe to see "
+                   + "their usage here.";
+        }
+        int totalInput = 0;
+        int totalOutput = 0;
+        int totalTokens = 0;
+        long totalLatency = 0;
+        int tuiRequests = 0;
+        int routeRequests = 0;
+        Map<String, long[]> perModel = new LinkedHashMap<>();
+        for (AiUsageEntry e : entries) {
+            totalInput += e.inputTokens();
+            totalOutput += e.outputTokens();
+            totalTokens += e.totalTokens();
+            totalLatency += e.latencyMs();
+            if (e.source() == AiUsageSource.ROUTE) {
+                routeRequests++;
+            } else {
+                tuiRequests++;
+            }
+            long[] stats = perModel.computeIfAbsent(modelTableKey(e), k -> new 
long[5]);
+            stats[0]++;
+            stats[1] += e.inputTokens();
+            stats[2] += e.outputTokens();
+            stats[3] += e.totalTokens();
+            stats[4] += e.latencyMs();
+        }
+        StringBuilder sb = new StringBuilder();
+        sb.append("Requests: ").append(entries.size());
+        if (routeRequests > 0) {
+            sb.append(" (").append(tuiRequests).append(" from this panel, 
").append(routeRequests)
+                    .append(" from routes)");
+        }
+        sb.append(", tokens: ").append(LlmClient.formatTokens(totalTokens))
+                .append(" (in ").append(LlmClient.formatTokens(totalInput))
+                .append(", out ").append(LlmClient.formatTokens(totalOutput))
+                .append("), avg latency: ").append(totalLatency / 
entries.size()).append(" ms\n");
+        for (Map.Entry<String, long[]> entry : perModel.entrySet()) {
+            long[] stats = entry.getValue();
+            sb.append("- ").append(entry.getKey()).append(": 
").append(stats[0]).append(" request(s), ")
+                    .append(LlmClient.formatTokens((int) stats[3])).append(" 
tokens (in ")
+                    .append(LlmClient.formatTokens((int) stats[1])).append(", 
out ")
+                    .append(LlmClient.formatTokens((int) stats[2])).append("), 
avg ")
+                    .append(stats[4] / stats[0]).append(" ms\n");
+        }
+        AiUsageEntry last = usageHistory.isEmpty() ? null : 
usageHistory.get(usageHistory.size() - 1);
+        if (last != null) {
+            sb.append("Last request: 
").append(LlmClient.formatTokens(last.totalTokens())).append(" tokens in ")
+                    .append(last.latencyMs()).append(" ms\n");
+        }
+        sb.append("Ctrl+U opens the full usage view with the per-turn chart.");
+        return sb.toString().strip();
+    }
+
+    private void toggleUsageView() {
+        statsView = !statsView;
+        statsScrollOffset = 0;
+        if (statsView) {
+            spanRefreshRequested = true;
+        }
+    }
+
     private String executeTuiTool(String name, JsonObject args) {
         if (toolRegistry == null) {
             return "Error: TUI tools not available";
@@ -2168,6 +2477,41 @@ class AiPanel {
             return true;
         }
 
+        @Override
+        public String describeContext() {
+            return AiPanel.this.describeContext();
+        }
+
+        @Override
+        public String compactHistoryNow() {
+            return AiPanel.this.compactHistoryNow();
+        }
+
+        @Override
+        public boolean retryLastQuestion() {
+            return AiPanel.this.retryLastQuestion();
+        }
+
+        @Override
+        public String usageSummary() {
+            return AiPanel.this.usageSummary();
+        }
+
+        @Override
+        public void copyLastResponse() {
+            copyLastResponseToClipboard();
+        }
+
+        @Override
+        public void exportConversation() {
+            exportChatToFile();
+        }
+
+        @Override
+        public String systemPrompt() {
+            return buildSystemPrompt();
+        }
+
         @Override
         public CompletableFuture<AiCliCommandExecutor.Result> 
executeCli(AiCliCommandExecutor.Request request) {
             return cliCommandExecutor.executeAsync(request);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
index 3afa1cc4b74b..81891992f4b6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
@@ -47,6 +47,38 @@ interface AiSlashCommandContext {
      */
     String describeToolMode();
 
+    /**
+     * Multi-line summary of what the next request will cost: provider and 
model, tool set, static prefix size,
+     * conversation history size and the session total so far.
+     */
+    String describeContext();
+
+    /**
+     * Compacts the model history now (older tool results shrunk, oldest turns 
dropped) and returns a one-line summary
+     * of the effect.
+     */
+    String compactHistoryNow();
+
+    /**
+     * Resends the last question. Returns {@code false} when there is no 
question to retry or no client to send it to.
+     */
+    boolean retryLastQuestion();
+
+    /**
+     * Text summary of the AI usage so far (requests, tokens, latency, per 
model), the same figures the Ctrl+U view
+     * shows.
+     */
+    String usageSummary();
+
+    void copyLastResponse();
+
+    void exportConversation();
+
+    /**
+     * The system prompt the panel sends with every request.
+     */
+    String systemPrompt();
+
     /**
      * Switches the tool mode to {@code auto}, {@code core} or {@code full} 
and persists it. Returns {@code false} when
      * the mode is not one of those values.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
index 9b4f580c83fd..c3535e64433a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
@@ -71,6 +71,37 @@ final class AiSlashCommandRegistry {
         commands.add(new Descriptor(
                 "tools", List.of("t"), "Show or switch the tool set sent to 
the model", "[auto|core|full]",
                 AiSlashCommandRegistry::executeTools));
+        commands.add(new Descriptor(
+                "context", List.of("ctx"), "Show what the next request costs: 
provider, tools, prompt and history size",
+                null,
+                (context, arguments) -> 
CommandResult.system(context.describeContext())));
+        commands.add(new Descriptor(
+                "compact", List.of(), "Shrink the conversation history sent to 
the model", null,
+                (context, arguments) -> 
CommandResult.system(context.compactHistoryNow())));
+        commands.add(new Descriptor(
+                "retry", List.of(), "Send the last question again", null,
+                (context, arguments) -> context.retryLastQuestion()
+                        ? CommandResult.system("")
+                        : CommandResult.error("No question to retry. Ask 
something first.")));
+        commands.add(new Descriptor(
+                "usage", List.of("u"), "Show AI usage so far: requests, 
tokens, latency (Ctrl+U opens the full view)",
+                null,
+                (context, arguments) -> 
CommandResult.system(context.usageSummary())));
+        commands.add(new Descriptor(
+                "copy", List.of("y"), "Copy the last AI response to the 
clipboard (Ctrl+Y)", null,
+                (context, arguments) -> {
+                    context.copyLastResponse();
+                    return CommandResult.system("");
+                }));
+        commands.add(new Descriptor(
+                "export", List.of("e"), "Export the conversation to a Markdown 
file (Ctrl+E)", null,
+                (context, arguments) -> {
+                    context.exportConversation();
+                    return CommandResult.system("");
+                }));
+        commands.add(new Descriptor(
+                "prompt", List.of(), "Show the system prompt sent to the 
model", null,
+                (context, arguments) -> 
CommandResult.system(context.systemPrompt())));
         commands.add(new Descriptor(
                 "clear", List.of("c"), "Clear the conversation", null,
                 (context, arguments) -> {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
index e217e2307a8f..e63d3b766e95 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -66,11 +66,11 @@ class CatalogTab extends AbstractTableTab {
     private int scopeIndex;
     private boolean fullCatalog;
     private CamelCatalog catalog;
-    private List<CatalogEntry> allEntries = Collections.emptyList();
-    private List<CatalogEntry> filteredEntries = Collections.emptyList();
+    private volatile List<CatalogEntry> allEntries = Collections.emptyList();
+    private volatile List<CatalogEntry> filteredEntries = 
Collections.emptyList();
     private String lastPid;
-    private String errorMessage;
-    private boolean dataLoaded;
+    private volatile String errorMessage;
+    private volatile boolean dataLoaded;
 
     CatalogTab(MonitorContext ctx) {
         super(ctx, "name", "kind", "description");
@@ -89,6 +89,23 @@ class CatalogTab extends AbstractTableTab {
         }
     }
 
+    @Override
+    public boolean ensureDataLoaded() {
+        onTabSelected();
+        return true;
+    }
+
+    @Override
+    public String dataLoadError() {
+        if (!dataLoaded) {
+            return null;
+        }
+        if (errorMessage != null) {
+            return errorMessage;
+        }
+        return allEntries.isEmpty() ? "No catalog entries for the selected 
integration" : null;
+    }
+
     @Override
     public void onIntegrationChanged() {
         allEntries = Collections.emptyList();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index 66635b2c3323..e85a27be369f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -60,11 +60,11 @@ class ClasspathTab extends AbstractTab {
     private TextInputState filterInputState = new TextInputState("");
     private String filterTerm;
     private int scopeIndex;
-    private List<JarEntry> allEntries = Collections.emptyList();
-    private List<JarEntry> filteredEntries = Collections.emptyList();
+    private volatile List<JarEntry> allEntries = Collections.emptyList();
+    private volatile List<JarEntry> filteredEntries = Collections.emptyList();
     private String lastPid;
-    private String errorMessage;
-    private boolean dataLoaded;
+    private volatile String errorMessage;
+    private volatile boolean dataLoaded;
 
     ClasspathTab(MonitorContext ctx) {
         super(ctx);
@@ -83,6 +83,23 @@ class ClasspathTab extends AbstractTab {
         }
     }
 
+    @Override
+    public boolean ensureDataLoaded() {
+        onTabSelected();
+        return true;
+    }
+
+    @Override
+    public String dataLoadError() {
+        if (!dataLoaded) {
+            return null;
+        }
+        if (errorMessage != null) {
+            return errorMessage;
+        }
+        return allEntries.isEmpty() ? "No JARs on the classpath of the 
selected integration" : null;
+    }
+
     @Override
     public void onIntegrationChanged() {
         allEntries = Collections.emptyList();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
index f8771b1cbcbf..ed99acccf437 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
@@ -70,10 +70,10 @@ class CveAuditTab extends AbstractTableTab {
     private int detailScroll;
 
     private List<DependencyLoader.DepEntry> depEntries = 
Collections.emptyList();
-    private List<VulnGroup> allGroups = Collections.emptyList();
+    private volatile List<VulnGroup> allGroups = Collections.emptyList();
     private String lastPid;
-    private String errorMessage;
-    private boolean dataLoaded;
+    private volatile String errorMessage;
+    private volatile boolean dataLoaded;
     private int scannedCount;
 
     CveAuditTab(MonitorContext ctx) {
@@ -98,6 +98,23 @@ class CveAuditTab extends AbstractTableTab {
         }
     }
 
+    @Override
+    public boolean ensureDataLoaded() {
+        onTabSelected();
+        return true;
+    }
+
+    @Override
+    public String dataLoadError() {
+        if (!dataLoaded) {
+            return null;
+        }
+        if (errorMessage != null) {
+            return errorMessage;
+        }
+        return allGroups.isEmpty() ? "No known vulnerabilities found for the 
selected integration" : null;
+    }
+
     @Override
     public void onIntegrationChanged() {
         allGroups = Collections.emptyList();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
index 2a75a6523991..9065f7eb328e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
@@ -446,7 +446,9 @@ class DocViewerPopup {
                                  + "    export LLM_API_KEY=any-value\n"
                                  + "    export 
LLM_BASE_URL=http://localhost:1234\n";
                                  + "    camel tui\n\n"
-                                 + "`OPENAI_BASE_URL` is accepted as an 
alternative to `LLM_BASE_URL`.\n\n"
+                                 + "`OPENAI_BASE_URL` is accepted as an 
alternative to `LLM_BASE_URL`. The model is the first one\n"
+                                 + "the server lists on `/v1/models`; pick 
another with `/model <name>` in the panel or\n"
+                                 + "*AI Model* in **F2 -> Settings**. The 
model must support tool calling.\n\n"
                                  + "## 4. Using the AI panel\n\n"
                                  + "- **F8** opens and closes the panel; 
**Enter** sends the prompt\n"
                                  + "- **Ctrl+P** (or `/provider`) switches 
provider or model for the session\n"
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
index 828b12368248..42f6c77908d6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
@@ -61,11 +61,11 @@ class MavenDependenciesTab extends AbstractTableTab {
     private TextInputState filterInputState = new TextInputState("");
     private String filterTerm;
     private int scopeIndex;
-    private List<DependencyLoader.DepEntry> allEntries = 
Collections.emptyList();
-    private List<DependencyLoader.DepEntry> filteredEntries = 
Collections.emptyList();
+    private volatile List<DependencyLoader.DepEntry> allEntries = 
Collections.emptyList();
+    private volatile List<DependencyLoader.DepEntry> filteredEntries = 
Collections.emptyList();
     private String lastPid;
-    private String errorMessage;
-    private boolean dataLoaded;
+    private volatile String errorMessage;
+    private volatile boolean dataLoaded;
     private String dataSource;
     private boolean transitiveMode;
     private boolean transitiveLoading;
@@ -89,6 +89,23 @@ class MavenDependenciesTab extends AbstractTableTab {
         }
     }
 
+    @Override
+    public boolean ensureDataLoaded() {
+        onTabSelected();
+        return true;
+    }
+
+    @Override
+    public String dataLoadError() {
+        if (!dataLoaded) {
+            return null;
+        }
+        if (errorMessage != null) {
+            return errorMessage;
+        }
+        return allEntries.isEmpty() ? "No Maven dependencies found for the 
selected integration" : null;
+    }
+
     @Override
     public void onIntegrationChanged() {
         allEntries = Collections.emptyList();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index db75bc20437f..efe2b592a1fb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -448,17 +448,54 @@ class McpFacade {
 
     // ---- Data access ----
 
+    /** How long a table read waits for a tab that loads its data on demand. 
The connector action timeout is 5s. */
+    static final long ON_DEMAND_LOAD_TIMEOUT_MS = 8_000;
+
     JsonObject getTableData(String tabName) {
-        MonitorTab tab;
+        MonitorTab tab = resolveTab(tabName);
+        return tab != null ? awaitTableData(tab, ON_DEMAND_LOAD_TIMEOUT_MS) : 
null;
+    }
+
+    /**
+     * Why {@link #getTableData(String)} returned nothing for the tab: unknown 
tab, a load error, or an empty tab.
+     */
+    String tableDataError(String tabName) {
+        MonitorTab tab = resolveTab(tabName);
+        if (tab == null) {
+            return tabName != null && !tabName.isBlank() ? "Unknown tab: " + 
tabName : "No active tab";
+        }
+        String error = tab.dataLoadError();
+        return error != null ? error : "No table data available for tab: " + 
tabName;
+    }
+
+    private MonitorTab resolveTab(String tabName) {
         if (tabName != null && !tabName.isBlank()) {
-            tab = tabRegistry.findTabByName(tabName);
-            if (tab == null) {
-                return null;
+            return tabRegistry.findTabByName(tabName);
+        }
+        return bridge.activeTab();
+    }
+
+    /**
+     * Reads the tab's table, and when the tab loads its data on demand and 
has none yet, starts the load and waits
+     * (polling) until data arrives, the load reports an error or an empty 
result, or the timeout passes. Must not be
+     * called on the render thread, since the loads complete there.
+     */
+    static JsonObject awaitTableData(MonitorTab tab, long timeoutMs) {
+        JsonObject data = tab.getTableDataAsJson();
+        if (data != null || !tab.ensureDataLoaded()) {
+            return data;
+        }
+        long deadline = System.currentTimeMillis() + timeoutMs;
+        while (data == null && System.currentTimeMillis() < deadline && 
tab.dataLoadError() == null) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
             }
-        } else {
-            tab = bridge.activeTab();
+            data = tab.getTableDataAsJson();
         }
-        return tab != null ? tab.getTableDataAsJson() : null;
+        return data;
     }
 
     boolean executeAction(String actionName) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
index 673fc5474058..9beaf7cf6e32 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
@@ -68,6 +68,24 @@ interface MonitorTab {
     default void onTabSelected() {
     }
 
+    /**
+     * For tabs that fetch their data only when opened (classpath, 
dependencies, catalog, CVE audit, startup): starts
+     * the fetch for the selected integration if it has not happened yet and 
returns {@code true}, so a caller that
+     * reads the tab without opening it (the AI panel, an MCP client) can wait 
for the data. Tabs whose data is always
+     * current return {@code false}.
+     */
+    default boolean ensureDataLoaded() {
+        return false;
+    }
+
+    /**
+     * After an on-demand load finished without producing table data: the 
error, or a message saying the tab is empty.
+     * {@code null} while the load is still running or when the tab does not 
load on demand.
+     */
+    default String dataLoadError() {
+        return null;
+    }
+
     default void onIntegrationChanged() {
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
index 25a6b2428528..43311f36a944 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -53,14 +53,14 @@ class StartupTab extends AbstractTab {
     private final ScrollbarState scrollbarState = new ScrollbarState();
     private final AtomicBoolean loading = new AtomicBoolean(false);
 
-    private List<StartupStep> steps = Collections.emptyList();
+    private volatile List<StartupStep> steps = Collections.emptyList();
     private int scrollOffset;
     private long totalDuration;
     private long maxDuration;
     private long minDurationColor;
     private long maxDurationColor;
-    private String errorMessage;
-    private boolean dataLoaded;
+    private volatile String errorMessage;
+    private volatile boolean dataLoaded;
 
     StartupTab(MonitorContext ctx) {
         super(ctx);
@@ -73,6 +73,23 @@ class StartupTab extends AbstractTab {
         }
     }
 
+    @Override
+    public boolean ensureDataLoaded() {
+        onTabSelected();
+        return true;
+    }
+
+    @Override
+    public String dataLoadError() {
+        if (!dataLoaded) {
+            return null;
+        }
+        if (errorMessage != null) {
+            return errorMessage;
+        }
+        return steps.isEmpty() ? "No startup data available for the selected 
integration" : null;
+    }
+
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
         if (ke.isUp()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 641dc13b50f2..147a52da3dce 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -427,9 +427,13 @@ class TuiToolRegistry {
                 Map.of())));
         tools.add(toToolDef(toolDef(
                 "tui_send_message",
-                "Sends a message to a Camel endpoint in the selected 
integration. "
-                                    + "Uses the file-based IPC protocol to 
deliver the message directly.",
-                Map.of("endpoint", propDef("string", "Endpoint URI to send to 
(e.g. 'direct:myRoute', 'seda:queue')"),
+                "Sends a message to any Camel endpoint URI from inside the 
selected integration: direct:/seda: "
+                                    + "to feed a route, or a producer such as 
paho-mqtt5:, kafka:, jms:, http:, file: "
+                                    + "to publish to the system a route 
consumes from. A route that only consumes from "
+                                    + "a broker has no direct: endpoint; 
publish to the broker with the same component "
+                                    + "and options the route uses.",
+                Map.of("endpoint", propDef("string",
+                        "Endpoint URI, e.g. 'direct:myRoute', 
'paho-mqtt5:temperature?brokerUrl=tcp://localhost:1883'"),
                         "body", propDef("string", "Message body to send"),
                         "headers", propDef("string", "Message headers as 
key=value pairs separated by newlines")),
                 List.of("endpoint"))));
@@ -1301,7 +1305,7 @@ class TuiToolRegistry {
         String tab = args.get("tab") instanceof String s ? s : null;
         JsonObject data = facade.getTableData(tab);
         if (data == null) {
-            return "No table data available" + (tab != null ? " for tab: " + 
tab : "");
+            return facade.tableDataError(tab);
         }
         return Jsoner.serialize(data);
     }
@@ -1461,9 +1465,82 @@ class TuiToolRegistry {
         if (response == null) {
             return "Error: no integration selected or PID unavailable";
         }
+        String hint = unknownSchemeHint(endpoint, Jsoner.serialize(response));
+        if (hint != null) {
+            response.put("hint", hint);
+        }
         return Jsoner.serialize(response);
     }
 
+    /**
+     * When a send failed because the endpoint scheme is not a Camel component 
(a model guessing {@code mqt t:} for
+     * {@code paho-mqtt5:}), names the catalog components that look like what 
was meant so the next call can use one.
+     */
+    private String unknownSchemeHint(String endpoint, String result) {
+        int colon = endpoint.indexOf(':');
+        if (colon <= 0 || result == null) {
+            return null;
+        }
+        String lower = result.toLowerCase();
+        if (!(lower.contains("no component found") || 
lower.contains("nosuchendpoint")
+                || lower.contains("failed to resolve endpoint") || 
lower.contains("cannot find component"))) {
+            return null;
+        }
+        String scheme = endpoint.substring(0, colon).toLowerCase();
+        List<String> names;
+        try {
+            CamelCatalog catalog = CatalogLoader.loadCatalog(null, 
facade.getSelectedCamelVersion(), true);
+            names = catalog.findComponentNames();
+        } catch (Exception e) {
+            return null;
+        }
+        if (names.contains(scheme)) {
+            return null;
+        }
+        List<String> similar = suggestComponents(scheme, names);
+        if (similar.isEmpty()) {
+            return "Camel has no component named '" + scheme + "'. Use 
tui_catalog_doc to find the right component, "
+                   + "then send again with its scheme.";
+        }
+        return "Camel has no component named '" + scheme + "'. Similar 
components in the catalog: "
+               + String.join(", ", similar) + ". Send again with one of those 
schemes, e.g. '" + similar.get(0)
+               + endpoint.substring(colon) + "'.";
+    }
+
+    /** Well-known names people use for a protocol that differ from the Camel 
component name. */
+    private static final Map<String, List<String>> SCHEME_ALIASES = Map.of(
+            "mqtt", List.of("paho-mqtt5", "paho"),
+            "mqtt5", List.of("paho-mqtt5"),
+            "rabbitmq", List.of("spring-rabbitmq"),
+            "amq", List.of("activemq", "jms"),
+            "rest", List.of("rest", "platform-http", "http"),
+            "s3", List.of("aws2-s3"),
+            "sqs", List.of("aws2-sqs"),
+            "sns", List.of("aws2-sns"),
+            "pubsub", List.of("google-pubsub"),
+            "servicebus", List.of("azure-servicebus"));
+
+    /**
+     * Catalog component names that resemble the scheme: known aliases first, 
then names containing the scheme (or
+     * contained in it), at most five.
+     */
+    static List<String> suggestComponents(String scheme, List<String> 
componentNames) {
+        List<String> result = new ArrayList<>();
+        for (String alias : SCHEME_ALIASES.getOrDefault(scheme, List.of())) {
+            if (componentNames.contains(alias) && !result.contains(alias)) {
+                result.add(alias);
+            }
+        }
+        if (scheme.length() >= 3) {
+            for (String name : componentNames) {
+                if ((name.contains(scheme) || scheme.contains(name)) && 
!name.equals(scheme) && !result.contains(name)) {
+                    result.add(name);
+                }
+            }
+        }
+        return result.size() > 5 ? result.subList(0, 5) : result;
+    }
+
     private String callExecuteSql(Map<String, Object> args) {
         String query = (String) args.get("query");
         if (query == null || query.isBlank()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelHistoryCompactionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelHistoryCompactionTest.java
index a5e842baed3e..48a9c250a592 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelHistoryCompactionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelHistoryCompactionTest.java
@@ -78,6 +78,19 @@ class AiPanelHistoryCompactionTest {
         assertEquals("call-q1", 
history.get(2).toolResults().get(0).toolCallId());
     }
 
+    @Test
+    void forcedCompactionAlsoShrinksThePreviousTurn() {
+        List<LlmClient.Message> history = new ArrayList<>();
+        history.addAll(turn("q1", BIG));
+        history.addAll(turn("q2", BIG));
+
+        AiPanel.compactHistory(history, 20, 400, false);
+
+        assertTrue(toolResultContent(history.get(2)).contains("[earlier result 
compacted"));
+        assertTrue(toolResultContent(history.get(6)).contains("[earlier result 
compacted"));
+        assertEquals(8, history.size());
+    }
+
     @Test
     void dropsWholeOldestTurnsBeyondTheLimit() {
         List<LlmClient.Message> history = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelOllamaBenchmarkTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelOllamaBenchmarkTest.java
new file mode 100644
index 000000000000..b0b5c8dc4be4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelOllamaBenchmarkTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * Sends the AI panel's real payload (system prompt, tool schemas, a user 
question) to a local Ollama and prints how
+ * long the model spends on prompt processing and generation, cold and warm, 
in core and full tool mode. Skipped unless
+ * {@code CAMEL_TUI_OLLAMA_BENCH} names the model to use, for example:
+ *
+ * <pre>
+ * CAMEL_TUI_OLLAMA_BENCH=qwen3.6:35b-a3b mvn test 
-Dtest=AiPanelOllamaBenchmarkTest
+ * </pre>
+ *
+ * {@code OLLAMA_HOST} overrides the server URL (default {@code 
http://localhost:11434}). The request mirrors what
+ * {@code LlmClient} sends: {@code think=false}, {@code keep_alive=30m}, 
{@code num_ctx} as the client would set it. See
+ * the module README for prerequisites and how to read the output.
+ */
+class AiPanelOllamaBenchmarkTest {
+
+    private static final String[] QUESTIONS = {
+            "[Monitoring timer-log (PID 74824)]\nwhat model is this",
+            "[Monitoring timer-log (PID 74824)]\nwhat routes are running?",
+            "[Monitoring kafka-demo (PID 80011)]\nany errors?" };
+
+    record Sample(String label, long promptTokens, double promptEvalSeconds, 
double loadSeconds, long genTokens,
+            double genSeconds, double wallSeconds, String toolCall) {
+
+        @Override
+        public String toString() {
+            return String.format(Locale.ROOT,
+                    "  %-24s prompt=%5d tok  prompt_eval=%5.1fs (%5.0f tok/s)  
load=%4.1fs  gen=%3d tok in %4.1fs  wall=%5.1fs%s",
+                    label, promptTokens, promptEvalSeconds, promptTokens / 
Math.max(promptEvalSeconds, 0.001),
+                    loadSeconds, genTokens, genSeconds, wallSeconds, toolCall 
!= null ? "  tool=" + toolCall : "");
+        }
+    }
+
+    @Test
+    void benchmarkRealPromptAgainstLocalOllama() throws Exception {
+        String model = System.getenv("CAMEL_TUI_OLLAMA_BENCH");
+        assumeTrue(model != null && !model.isBlank(), "set 
CAMEL_TUI_OLLAMA_BENCH=<model> to run the benchmark");
+        String host = System.getenv("OLLAMA_HOST");
+        String url = (host != null && !host.isBlank() ? host : 
"http://localhost:11434";);
+        if (!url.startsWith("http")) {
+            url = "http://"; + url;
+        }
+        HttpClient http = 
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+
+        System.out.println("AI panel benchmark against " + url + " with " + 
model);
+        for (String mode : List.of(AiPanel.TOOL_MODE_FULL, 
AiPanel.TOOL_MODE_CORE)) {
+            AiPanel panel = new AiPanel();
+            panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+            panel.setToolModeForTesting(mode);
+            String system = panel.systemPromptForTesting();
+            JsonArray tools = wireTools(panel.toolDefinitionsForTesting());
+            System.out.println("== " + mode + ": " + tools.size() + " tools 
==");
+            String[] labels = { "1st (cold prefix)", "2nd (warm prefix)", "3rd 
(switched integr.)" };
+            for (int i = 0; i < QUESTIONS.length; i++) {
+                Sample sample = chat(http, url, model, system, tools, 
QUESTIONS[i], labels[i]);
+                assertNotNull(sample);
+                System.out.println(sample);
+            }
+        }
+    }
+
+    private static JsonArray wireTools(List<LlmClient.ToolDef> defs) {
+        JsonArray tools = new JsonArray();
+        for (LlmClient.ToolDef def : defs) {
+            JsonObject function = new JsonObject();
+            function.put("name", def.name());
+            function.put("description", def.description());
+            function.put("parameters", def.parameters());
+            JsonObject tool = new JsonObject();
+            tool.put("type", "function");
+            tool.put("function", function);
+            tools.add(tool);
+        }
+        return tools;
+    }
+
+    private static Sample chat(
+            HttpClient http, String url, String model, String system, 
JsonArray tools, String question, String label)
+            throws Exception {
+        JsonArray messages = new JsonArray();
+        messages.add(message("system", system));
+        messages.add(message("user", question));
+        JsonObject options = new JsonObject();
+        options.put("temperature", 0.3);
+        options.put("num_ctx", 32768);
+        JsonObject request = new JsonObject();
+        request.put("model", model);
+        request.put("messages", messages);
+        request.put("tools", tools);
+        request.put("stream", false);
+        request.put("think", false);
+        request.put("keep_alive", "30m");
+        request.put("options", options);
+
+        long start = System.nanoTime();
+        HttpResponse<String> response = http.send(
+                HttpRequest.newBuilder(URI.create(url + "/api/chat"))
+                        .timeout(Duration.ofMinutes(15))
+                        .header("Content-Type", "application/json")
+                        
.POST(HttpRequest.BodyPublishers.ofString(request.toJson(), 
StandardCharsets.UTF_8))
+                        .build(),
+                HttpResponse.BodyHandlers.ofString());
+        double wall = (System.nanoTime() - start) / 1e9;
+        JsonObject body = (JsonObject) Jsoner.deserialize(response.body());
+        String toolCall = null;
+        JsonObject message = (JsonObject) body.get("message");
+        if (message != null && message.get("tool_calls") instanceof JsonArray 
calls && !calls.isEmpty()) {
+            JsonObject first = (JsonObject) calls.get(0);
+            toolCall = (String) ((JsonObject) 
first.get("function")).get("name");
+        }
+        return new Sample(
+                label,
+                number(body, "prompt_eval_count"),
+                number(body, "prompt_eval_duration") / 1e9,
+                number(body, "load_duration") / 1e9,
+                number(body, "eval_count"),
+                number(body, "eval_duration") / 1e9,
+                wall, toolCall);
+    }
+
+    private static JsonObject message(String role, String content) {
+        JsonObject message = new JsonObject();
+        message.put("role", role);
+        message.put("content", content);
+        return message;
+    }
+
+    private static long number(JsonObject body, String key) {
+        return body.get(key) instanceof Number n ? n.longValue() : 0L;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
new file mode 100644
index 000000000000..8fe124041f08
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.List;
+
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Guards the size of the static prefix the AI panel sends with every request 
(system prompt plus tool schemas). A local
+ * model pays for every token of it in prompt-processing time on every 
question, so a regression here is a latency
+ * regression for everyone running Ollama. The budgets leave headroom over the 
measured values; if a change genuinely
+ * needs more, raise the budget in the same commit and say why.
+ */
+class AiPanelPromptBudgetTest {
+
+    /** Measured ~3.0k tokens for 19 core tools. */
+    static final int CORE_BUDGET_TOKENS = 3_500;
+    /** Measured ~6.9k tokens for 47 tools. */
+    static final int FULL_BUDGET_TOKENS = 7_500;
+
+    record Prefix(String mode, int tools, long promptChars, long toolChars) {
+
+        int promptTokens() {
+            return AiPanel.estimateTokens(promptChars);
+        }
+
+        int toolTokens() {
+            return AiPanel.estimateTokens(toolChars);
+        }
+
+        int totalTokens() {
+            return promptTokens() + toolTokens();
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%-4s tools=%2d  system prompt ~%d tok  tool 
schemas ~%d tok  total ~%d tok",
+                    mode, tools, promptTokens(), toolTokens(), totalTokens());
+        }
+    }
+
+    /**
+     * Serializes the tools the way {@code LlmClient.buildOpenAiStyleTools} 
sends them, so the count matches the wire.
+     */
+    static long wireChars(List<LlmClient.ToolDef> defs) {
+        long chars = 0;
+        for (LlmClient.ToolDef def : defs) {
+            JsonObject function = new JsonObject();
+            function.put("name", def.name());
+            function.put("description", def.description());
+            function.put("parameters", def.parameters());
+            JsonObject tool = new JsonObject();
+            tool.put("type", "function");
+            tool.put("function", function);
+            chars += tool.toJson().length();
+        }
+        return chars;
+    }
+
+    static Prefix measure(String mode) {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        panel.setToolModeForTesting(mode);
+        List<LlmClient.ToolDef> defs = panel.toolDefinitionsForTesting();
+        return new Prefix(mode, defs.size(), 
panel.systemPromptForTesting().length(), wireChars(defs));
+    }
+
+    @Test
+    void corePrefixStaysWithinBudget() {
+        Prefix core = measure(AiPanel.TOOL_MODE_CORE);
+        System.out.println("AI panel static prefix: " + core);
+
+        assertTrue(core.totalTokens() <= CORE_BUDGET_TOKENS,
+                "core prefix grew to ~" + core.totalTokens() + " tokens, 
budget " + CORE_BUDGET_TOKENS + ": " + core);
+    }
+
+    @Test
+    void fullPrefixStaysWithinBudget() {
+        Prefix full = measure(AiPanel.TOOL_MODE_FULL);
+        System.out.println("AI panel static prefix: " + full);
+
+        assertTrue(full.totalTokens() <= FULL_BUDGET_TOKENS,
+                "full prefix grew to ~" + full.totalTokens() + " tokens, 
budget " + FULL_BUDGET_TOKENS + ": " + full);
+    }
+
+    @Test
+    void systemPromptStaysShortAndFreeOfTheToolList() {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        panel.setToolModeForTesting(AiPanel.TOOL_MODE_FULL);
+        String prompt = panel.systemPromptForTesting();
+
+        // the tool definitions already describe every tool; repeating them in 
prose doubles the cost
+        assertTrue(AiPanel.estimateTokens(prompt.length()) <= 450,
+                "system prompt grew to ~" + 
AiPanel.estimateTokens(prompt.length()) + " tokens");
+        assertTrue(!prompt.contains("- tui_get_table:"), "system prompt must 
not list the tools again");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index ad71a7d2ba4d..c7d98e0950ee 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.file.Path;
+import java.time.Instant;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
@@ -31,6 +32,7 @@ import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
 import org.apache.camel.dsl.jbang.core.commands.LlmClient;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -661,11 +663,11 @@ class AiPanelTest {
     void tabCompletesLongestCommonPrefixThenCyclesForward() {
         AiPanel panel = new AiPanel();
         panel.open();
-        type(panel, "/c");
+        type(panel, "/cle");
 
-        // /c matches /clear, /clear-history, and /close, so the first TAB 
fills in their common prefix.
+        // /cle matches /clear and /clear-history, so the first TAB fills in 
their common prefix.
         tab(panel);
-        assertEquals("/cl", panel.inputBufferForTesting());
+        assertEquals("/clear", panel.inputBufferForTesting());
 
         // No further prefix can be added, so subsequent TABs cycle through 
the matches and wrap around.
         tab(panel);
@@ -673,6 +675,21 @@ class AiPanelTest {
         tab(panel);
         assertEquals("/clear-history", panel.inputBufferForTesting());
         tab(panel);
+        assertEquals("/clear", panel.inputBufferForTesting());
+    }
+
+    @Test
+    void tabCyclesThroughMatchesWhenNoPrefixCanBeAdded() {
+        AiPanel panel = new AiPanel();
+        panel.open();
+        type(panel, "/cl");
+
+        // /cl matches /clear, /clear-history and /close and is already their 
common prefix, so TAB cycles.
+        tab(panel);
+        assertEquals("/clear", panel.inputBufferForTesting());
+        tab(panel);
+        assertEquals("/clear-history", panel.inputBufferForTesting());
+        tab(panel);
         assertEquals("/close", panel.inputBufferForTesting());
         tab(panel);
         assertEquals("/clear", panel.inputBufferForTesting());
@@ -682,11 +699,9 @@ class AiPanelTest {
     void shiftTabCyclesBackward() {
         AiPanel panel = new AiPanel();
         panel.open();
-        type(panel, "/c");
-        tab(panel);
-        assertEquals("/cl", panel.inputBufferForTesting());
+        type(panel, "/cl");
 
-        // Shift+TAB from the common prefix selects the last match, then walks 
backward through the list.
+        // Shift+TAB selects the last match, then walks backward through the 
list.
         shiftTab(panel);
         assertEquals("/close", panel.inputBufferForTesting());
         shiftTab(panel);
@@ -710,8 +725,7 @@ class AiPanelTest {
     void editingResetsCompletionCycle() {
         AiPanel panel = new AiPanel();
         panel.open();
-        type(panel, "/c");
-        tab(panel);
+        type(panel, "/cl");
         tab(panel);
         assertEquals("/clear", panel.inputBufferForTesting());
 
@@ -789,6 +803,133 @@ class AiPanelTest {
         assertEquals("/run --exam", panel.inputBufferForTesting());
     }
 
+    // ---- stuck tool loops ----
+
+    @Test
+    void repeatedIdenticalToolCallsEndTheTurnWithAnExplanation() throws 
Exception {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        LoopingLlmClient client = new LoopingLlmClient();
+        panel.setClientForTesting(client);
+        panel.open();
+        type(panel, "send a message to the mqtt topic");
+        panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
!panel.isAgentThreadRunningForTesting());
+
+        AiPanel.ConversationEntry last = 
panel.conversationForTesting().get(panel.conversationForTesting().size() - 1);
+        assertEquals(AiRole.ERROR, last.role());
+        assertTrue(last.text().contains("Reached maximum iterations"), 
last.text());
+        assertTrue(last.text().contains("tui_send_message"), last.text());
+        assertTrue(last.text().contains("AI Log"), last.text());
+        // after the third identical call the tool is no longer executed; the 
model is told to stop instead
+        assertTrue(client.sawStopNote, "the model must be told to stop 
repeating the call");
+        assertEquals(AiPanel.MAX_IDENTICAL_TOOL_CALLS, client.executedResults,
+                "the tool must not run again once the repeat limit is 
reached");
+    }
+
+    /** Always asks for the same tool call, like a model stuck on a failing 
send. */
+    private static final class LoopingLlmClient extends LlmClient {
+
+        volatile boolean sawStopNote;
+        volatile int executedResults;
+
+        LoopingLlmClient() {
+            withModel("test-model");
+            withApiType(ApiType.openai);
+        }
+
+        @Override
+        public boolean detectEndpoint() {
+            return true;
+        }
+
+        @Override
+        public ChatResponse chatWithTools(String systemPrompt, List<Message> 
messages, List<ToolDef> tools) {
+            Message lastMessage = messages.get(messages.size() - 1);
+            if (lastMessage.toolResults() != null) {
+                for (ToolResult result : lastMessage.toolResults()) {
+                    if (result.content().contains("Stop calling tools now")) {
+                        sawStopNote = true;
+                    } else {
+                        executedResults++;
+                    }
+                }
+            }
+            JsonObject args = new JsonObject();
+            args.put("endpoint", "direct:mqtt");
+            args.put("body", "25");
+            return new ChatResponse(
+                    null, List.of(new ToolCall("call-1", "tui_send_message", 
args)), "tool_use", false,
+                    TokenUsage.EMPTY);
+        }
+    }
+
+    // ---- /context and /retry ----
+
+    @Test
+    void contextDescribesProviderToolsPrefixAndHistory() {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        RecordingLlmClient client = new RecordingLlmClient("ok");
+        client.withApiType(LlmClient.ApiType.ollama);
+        panel.setClientForTesting(client);
+
+        String context = panel.describeContext();
+
+        assertTrue(context.contains("Provider: ollama"), context);
+        assertTrue(context.contains("(local)"), context);
+        assertTrue(context.contains("Tools: core ("), context);
+        assertTrue(context.contains("Static prefix: ~"), context);
+        assertTrue(context.contains("History: 0 turn(s)"), context);
+    }
+
+    @Test
+    void retryResendsTheLastQuestionFromACleanTurn() throws Exception {
+        AiPanel panel = new AiPanel();
+        RecordingLlmClient client = new RecordingLlmClient("first answer");
+        panel.setClientForTesting(client);
+        panel.open();
+        type(panel, "what routes are running?");
+        panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        assertTrue(client.awaitAnswer(5, TimeUnit.SECONDS));
+        await().atMost(5, TimeUnit.SECONDS).until(() -> 
!panel.isAgentThreadRunningForTesting());
+        int messagesAfterFirst = panel.messageCountForTesting();
+
+        assertTrue(panel.retryLastQuestion());
+        await().atMost(5, TimeUnit.SECONDS).until(() -> 
!panel.isAgentThreadRunningForTesting());
+
+        assertEquals("what routes are running?", client.lastQuestion());
+        // the retried turn replaced the earlier one in the model history 
instead of stacking on top of it
+        assertEquals(messagesAfterFirst, panel.messageCountForTesting());
+        assertEquals(2, panel.conversationForTesting().stream().filter(e -> 
e.role() == AiRole.USER).count());
+    }
+
+    @Test
+    void usageSummaryReportsTotalsPerModelAndLastRequest() {
+        AiPanel panel = new AiPanel();
+        panel.setClientForTesting(new RecordingLlmClient("ok"));
+        assertTrue(panel.usageSummary().startsWith("No AI usage yet"));
+
+        panel.recordUsageForTesting(new AiPanel.AiUsageEntry(
+                "qwen3.6:35b-a3b", "ollama", 3000, 100, 3100, 5000, 
"end_turn", Instant.now()));
+        panel.recordUsageForTesting(new AiPanel.AiUsageEntry(
+                "qwen3.6:35b-a3b", "ollama", 3200, 200, 3400, 2000, 
"end_turn", Instant.now()));
+
+        String summary = panel.usageSummary();
+
+        assertTrue(summary.startsWith("Requests: 2, tokens: 6.5k (in 6.2k, out 
300), avg latency: 3500 ms"), summary);
+        assertTrue(summary.contains("- [tui] qwen3.6:35b-a3b (ollama): 2 
request(s), 6.5k tokens"), summary);
+        assertTrue(summary.contains("Last request: 3.4k tokens in 2000 ms"), 
summary);
+    }
+
+    @Test
+    void retryWithoutAQuestionIsRefused() {
+        AiPanel panel = new AiPanel();
+        panel.setClientForTesting(new RecordingLlmClient("ok"));
+
+        assertFalse(panel.retryLastQuestion());
+    }
+
     // ---- tool set and system prompt tests ----
 
     @Test
@@ -986,6 +1127,39 @@ class AiPanelTest {
             return true;
         }
 
+        @Override
+        public String describeContext() {
+            return "";
+        }
+
+        @Override
+        public String compactHistoryNow() {
+            return "";
+        }
+
+        @Override
+        public boolean retryLastQuestion() {
+            return false;
+        }
+
+        @Override
+        public String usageSummary() {
+            return "";
+        }
+
+        @Override
+        public void copyLastResponse() {
+        }
+
+        @Override
+        public void exportConversation() {
+        }
+
+        @Override
+        public String systemPrompt() {
+            return "";
+        }
+
         boolean exitRequested;
         boolean cancelRequested;
         boolean providerSwitchRequested;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
index 30ecddcfaba9..c0759fc1bb69 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
@@ -34,8 +34,8 @@ class AiSlashCommandRegistryTest {
         AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
 
         assertEquals(
-                List.of("help", "provider", "model", "tools", "clear", 
"clear-history", "close", "quit", "run", "infra",
-                        "send"),
+                List.of("help", "provider", "model", "tools", "context", 
"compact", "retry", "usage", "copy", "export",
+                        "prompt", "clear", "clear-history", "close", "quit", 
"run", "infra", "send"),
                 
registry.descriptors().stream().map(AiSlashCommandRegistry.Descriptor::name).toList());
     }
 
@@ -75,7 +75,7 @@ class AiSlashCommandRegistryTest {
     void completionsIncludeAllCommandsForBareSlash() {
         AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
 
-        assertEquals(11, registry.completionsFor("/").size());
+        assertEquals(18, registry.completionsFor("/").size());
         assertFalse(registry.completionsFor("/").stream().anyMatch(descriptor 
-> "exit".equals(descriptor.name())));
     }
 
@@ -308,6 +308,41 @@ class AiSlashCommandRegistryTest {
         assertNull(context.switchedTo);
     }
 
+    @Test
+    void retryReportsWhenThereIsNothingToRetry() {
+        AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
+
+        AiSlashCommandRegistry.CommandResult result = 
registry.execute("/retry", new NoopSlashContext());
+
+        assertEquals(AiRole.ERROR, result.role());
+        assertTrue(result.text().contains("No question to retry"));
+    }
+
+    @Test
+    void contextCompactAndPromptRelayTheContextText() {
+        AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
+        NoopSlashContext context = new NoopSlashContext() {
+            @Override
+            public String describeContext() {
+                return "Provider: ollama";
+            }
+
+            @Override
+            public String compactHistoryNow() {
+                return "Compacted history";
+            }
+
+            @Override
+            public String systemPrompt() {
+                return "You are an Apache Camel assistant";
+            }
+        };
+
+        assertEquals("Provider: ollama", registry.execute("/ctx", 
context).text());
+        assertEquals("Compacted history", registry.execute("/compact", 
context).text());
+        assertEquals("You are an Apache Camel assistant", 
registry.execute("/prompt", context).text());
+    }
+
     private static final class ToolModeContext extends NoopSlashContext {
 
         private String switchedTo;
@@ -339,6 +374,39 @@ class AiSlashCommandRegistryTest {
             return false;
         }
 
+        @Override
+        public String describeContext() {
+            return "";
+        }
+
+        @Override
+        public String compactHistoryNow() {
+            return "";
+        }
+
+        @Override
+        public boolean retryLastQuestion() {
+            return false;
+        }
+
+        @Override
+        public String usageSummary() {
+            return "";
+        }
+
+        @Override
+        public void copyLastResponse() {
+        }
+
+        @Override
+        public void exportConversation() {
+        }
+
+        @Override
+        public String systemPrompt() {
+            return "";
+        }
+
         @Override
         public void closePanel() {
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeAwaitTableDataTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeAwaitTableDataTest.java
new file mode 100644
index 000000000000..5f9caae66590
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeAwaitTableDataTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class McpFacadeAwaitTableDataTest {
+
+    /** A tab whose data appears only some polls after the load was requested, 
like the classpath tab. */
+    private static class OnDemandTab implements MonitorTab {
+
+        final AtomicInteger loadRequests = new AtomicInteger();
+        final AtomicInteger reads = new AtomicInteger();
+        int readsUntilData = Integer.MAX_VALUE;
+        String error;
+
+        @Override
+        public boolean handleKeyEvent(KeyEvent ke) {
+            return false;
+        }
+
+        @Override
+        public void render(Frame frame, Rect area) {
+        }
+
+        @Override
+        public String description() {
+            return "test";
+        }
+
+        @Override
+        public void renderFooter(List<Span> spans) {
+        }
+
+        @Override
+        public JsonObject getTableDataAsJson() {
+            if (reads.incrementAndGet() >= readsUntilData) {
+                JsonObject data = new JsonObject();
+                data.put("tab", "Test");
+                return data;
+            }
+            return null;
+        }
+
+        @Override
+        public boolean ensureDataLoaded() {
+            loadRequests.incrementAndGet();
+            return true;
+        }
+
+        @Override
+        public String dataLoadError() {
+            return error;
+        }
+    }
+
+    @Test
+    void startsTheLoadAndWaitsForTheData() {
+        OnDemandTab tab = new OnDemandTab();
+        tab.readsUntilData = 3;
+
+        JsonObject data = McpFacade.awaitTableData(tab, 5_000);
+
+        assertNotNull(data);
+        assertEquals(1, tab.loadRequests.get());
+        assertTrue(tab.reads.get() >= 3);
+    }
+
+    @Test
+    void stopsWaitingWhenTheLoadReportsAnErrorOrEmptyResult() {
+        OnDemandTab tab = new OnDemandTab();
+        tab.error = "No response from integration";
+        long start = System.currentTimeMillis();
+
+        assertNull(McpFacade.awaitTableData(tab, 5_000));
+
+        assertTrue(System.currentTimeMillis() - start < 2_000, "must not wait 
for the whole timeout");
+        assertEquals(1, tab.loadRequests.get());
+    }
+
+    @Test
+    void givesUpAfterTheTimeout() {
+        OnDemandTab tab = new OnDemandTab();
+
+        assertNull(McpFacade.awaitTableData(tab, 300));
+        assertEquals(1, tab.loadRequests.get());
+    }
+
+    @Test
+    void tabsWithoutOnDemandLoadingAreReadOnce() {
+        OnDemandTab tab = new OnDemandTab() {
+            @Override
+            public boolean ensureDataLoaded() {
+                return false;
+            }
+        };
+
+        assertNull(McpFacade.awaitTableData(tab, 5_000));
+        assertEquals(1, tab.reads.get());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistrySuggestComponentsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistrySuggestComponentsTest.java
new file mode 100644
index 000000000000..95e48a617124
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistrySuggestComponentsTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TuiToolRegistrySuggestComponentsTest {
+
+    private static final List<String> NAMES = List.of("activemq", "aws2-s3", 
"aws2-sqs", "http", "jms", "kafka",
+            "paho", "paho-mqtt5", "platform-http", "rest", "spring-rabbitmq", 
"timer");
+
+    @Test
+    void knownAliasesComeFirst() {
+        assertEquals(List.of("paho-mqtt5", "paho"), 
TuiToolRegistry.suggestComponents("mqtt", NAMES));
+        assertEquals(List.of("spring-rabbitmq"), 
TuiToolRegistry.suggestComponents("rabbitmq", NAMES));
+        assertEquals(List.of("aws2-s3"), 
TuiToolRegistry.suggestComponents("s3", NAMES));
+    }
+
+    @Test
+    void substringMatchesFillInWhenThereIsNoAlias() {
+        assertEquals(List.of("http"), 
TuiToolRegistry.suggestComponents("https", NAMES));
+        assertEquals(List.of("paho", "paho-mqtt5"), 
TuiToolRegistry.suggestComponents("paho-mqtt", NAMES));
+        assertTrue(TuiToolRegistry.suggestComponents("xyz", NAMES).isEmpty());
+        // very short schemes would match too much, so they only get alias hits
+        assertTrue(TuiToolRegistry.suggestComponents("mq", NAMES).isEmpty());
+    }
+}

Reply via email to