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

commit f33abe089d7c230d17a3766894d290bf30d07fcb
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 8 16:19:08 2026 +0200

    CAMEL-24656: camel-jbang TUI - send a smaller, cache-friendly prompt from 
the AI panel to local models
    
    The F8 panel sent ~8.1k tokens of static prefix on every request: 46 tool
    schemas plus a system prompt that repeated the whole tool list in prose and
    embedded the selected integration and PID. Local models process prompts
    slowly, and the PID line invalidated the prompt cache whenever the selection
    changed.
    
    - Add TuiToolRegistry.CORE_TOOLS (18 tools for Q&A and troubleshooting) and
      send only those to local providers (Ollama, or any provider on localhost)
      by default. /tools [auto|core|full] shows or switches the set and persists
      it as camel.tui.ai.tools.
    - Drop the prose tool list from the system prompt (the tool definitions
      already carry the descriptions) and keep it free of per-turn state; the
      selected integration now travels in the user message.
    - Ask Ollama to keep the model loaded for 30 minutes and use a 32k context
      (OLLAMA_CONTEXT_LENGTH overrides) so follow-up turns hit the prompt cache
      instead of reloading the model or truncating the prompt.
    
    Measured static prefix per request: ~8.1k tokens before, ~3.1k tokens for
    local providers and ~7.3k for hosted providers after.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   8 +
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  16 ++
 .../camel/dsl/jbang/core/commands/LlmClient.java   |  67 +++++++-
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 173 ++++++++++++++-------
 .../core/commands/tui/AiSlashCommandContext.java   |  11 ++
 .../core/commands/tui/AiSlashCommandRegistry.java  |  14 ++
 .../dsl/jbang/core/commands/tui/TuiSettings.java   |  16 ++
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  22 ++-
 .../dsl/jbang/core/commands/tui/AiPanelTest.java   |  71 +++++++++
 .../commands/tui/AiSlashCommandRegistryTest.java   |  69 +++++++-
 .../jbang/core/commands/tui/TuiSettingsTest.java   |   2 +
 .../commands/tui/TuiToolRegistryCoreToolsTest.java |  61 ++++++++
 12 files changed, 466 insertions(+), 64 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index fb25b895e6e9..d845030dfef8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -612,6 +612,14 @@ shape. The `camel get variable` CLI command has already 
been updated accordingly
 
 === camel-jbang (TUI)
 
+The F8 AI panel now sends only a core subset of its `tui_*` tools to local 
providers (Ollama, or any
+provider on `localhost`); the drawing, animation and automation tools are left 
out to keep the
+prompt small for local models. Hosted providers are unaffected. Use `/tools 
full` in the panel, or
+set `camel.tui.ai.tools=full`, to restore the previous behaviour. Requests to 
Ollama now also set
+`keep_alive` to 30 minutes and `num_ctx` to 32768 (or `OLLAMA_CONTEXT_LENGTH` 
when that is set in the
+environment), which can cause a one-time model reload if the model was loaded 
with a different
+context size.
+
 `camel tui --record` is now rejected when combined with `--web`. The recording 
configuration applies
 to the whole process, so a browser session served by `--web` would be recorded 
into the same `.cast`
 file as the local session. Previously the combination was accepted, but 
recording never produced any
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 90c4d0b0f1ed..96cef22d9c49 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -814,6 +814,19 @@ NOTE: On Apple Silicon, use the default (GGUF) tags rather 
than the `-mlx` tags.
 engine cannot yet reuse the cached prompt for Qwen 3.x models, so every 
question re-processes the
 whole prompt, while the default engine reuses it and only processes what is 
new.
 
+==== Tool set for local models
+
+Every question sends the definitions of the `tui_*` tools the model may call, 
and a local model
+pays for each of them in prompt-processing time. The panel therefore sends 
only the core set of
+tools (state, tables, logs, errors, diagrams, topology, processor details, 
catalog docs, traces,
+spans, route control, sending messages, source files, navigation, log level 
and filters) to Ollama
+and to any provider on `localhost`, which roughly halves the prompt. Hosted 
providers get every
+tool, including the drawing, animation and automation tools. Use `/tools full` 
in the panel to send
+all tools to a local model too, `/tools core` to trim the set for a hosted 
one, or set
+`camel.tui.ai.tools` in `.camel-cli.properties`. Ollama requests also ask the 
server to keep the
+model loaded for 30 minutes and use a 32k context window 
(`OLLAMA_CONTEXT_LENGTH` overrides it),
+so follow-up questions reuse the cached prompt instead of reloading the model.
+
 ==== Using an OpenAI-compatible local server
 
 Set `LLM_API_KEY` and `LLM_BASE_URL` to connect to any OpenAI-compatible server
@@ -875,6 +888,9 @@ When the AI panel is open, input that starts with `/` runs 
a local panel command
 | `/model [model-name]` (`/m`)
 | Show the current model, or switch the session model.
 
+| `/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`.
+
 | `/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 1d8470f287e7..d472c0511bb4 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
@@ -53,6 +53,18 @@ public class LlmClient {
     private static final String DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6";
     private static final String DEFAULT_OPENAI_MODEL = "gpt-4o-mini";
     private static final String DEFAULT_OLLAMA_MODEL = "llama3.2";
+    /**
+     * Keep the model (and its prompt cache) loaded between turns of a 
conversation. Ollama's default of five minutes is
+     * shorter than a slow local answer plus the time the user spends reading 
it, after which the next request pays for
+     * a full model reload and re-processes the whole prompt.
+     */
+    private static final String OLLAMA_KEEP_ALIVE = "30m";
+    /**
+     * Context window requested from Ollama. The tool-calling system prompt 
alone is several thousand tokens, and older
+     * Ollama releases default to 4096 which silently truncates it; 32k leaves 
room for a long conversation with tool
+     * results while keeping the KV cache modest. {@code 
OLLAMA_CONTEXT_LENGTH} in the environment overrides it.
+     */
+    private static final int OLLAMA_NUM_CTX = 32768;
     private static final String DEFAULT_WATSONX_URL = 
"https://us-south.ml.cloud.ibm.com";;
     private static final String DEFAULT_WATSONX_MODEL = 
"ibm/granite-4-1-8b-instruct";
     private static final String DEFAULT_AZURE_API_VERSION = "2024-10-21";
@@ -194,6 +206,27 @@ public class LlmClient {
         return url;
     }
 
+    /**
+     * Whether the model runs on this machine: the Ollama provider, or any 
provider whose endpoint host is a loopback
+     * address (LM Studio, llama.cpp server, vLLM and similar 
OpenAI-compatible servers). Local models process prompts
+     * far slower than hosted ones, so callers use this to trim what they send 
per request.
+     */
+    public boolean isLocalEndpoint() {
+        if (apiType == ApiType.ollama) {
+            return true;
+        }
+        if (url == null) {
+            return false;
+        }
+        try {
+            String host = URI.create(url).getHost();
+            return host != null && (host.equalsIgnoreCase("localhost") || 
host.equals("127.0.0.1")
+                    || host.equals("::1") || host.equals("[::1]") || 
host.equals("0.0.0.0"));
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+
     // -- Builder --
 
     public static LlmClient create() {
@@ -559,10 +592,8 @@ public class LlmClient {
         request.put("prompt", userPrompt);
         request.put("system", systemPrompt);
         request.put("stream", stream);
-
-        JsonObject options = new JsonObject();
-        options.put("temperature", temperature);
-        request.put("options", options);
+        request.put("keep_alive", OLLAMA_KEEP_ALIVE);
+        request.put("options", ollamaOptions());
 
         if (stream) {
             return sendStreamingRequest(url + "/api/generate", request, null, 
"response");
@@ -936,6 +967,28 @@ public class LlmClient {
         return parseOpenAiChatResponse(response);
     }
 
+    private JsonObject ollamaOptions() {
+        JsonObject options = new JsonObject();
+        options.put("temperature", temperature);
+        options.put("num_ctx", ollamaNumCtx());
+        return options;
+    }
+
+    static int ollamaNumCtx() {
+        String env = System.getenv("OLLAMA_CONTEXT_LENGTH");
+        if (env != null && !env.isBlank()) {
+            try {
+                int value = Integer.parseInt(env.trim());
+                if (value > 0) {
+                    return value;
+                }
+            } catch (NumberFormatException e) {
+                // fall through to the default
+            }
+        }
+        return OLLAMA_NUM_CTX;
+    }
+
     // ---- Ollama native chat with tools ----
 
     private ChatResponse chatOllamaFormat(String systemPrompt, List<Message> 
messages, List<ToolDef> tools) {
@@ -949,10 +1002,8 @@ public class LlmClient {
         if (jsonTools != null) {
             request.put("tools", jsonTools);
         }
-
-        JsonObject options = new JsonObject();
-        options.put("temperature", temperature);
-        request.put("options", options);
+        request.put("keep_alive", OLLAMA_KEEP_ALIVE);
+        request.put("options", ollamaOptions());
 
         if (stream) {
             request.put("stream", true);
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 19caf4aecfd1..5e0107cafffc 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
@@ -76,6 +76,9 @@ class AiPanel {
     private static final DateTimeFormatter TIME_FMT
             = 
DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
     private static final String INPUT_PROMPT = "❯ ";
+    static final String TOOL_MODE_AUTO = "auto";
+    static final String TOOL_MODE_CORE = "core";
+    static final String TOOL_MODE_FULL = "full";
     private static final List<String> THINKING_VERBS = List.of(
             "Herding thoughts", "Chewing the cud", "Crossing the desert", 
"Loading the caravan",
             "Sniffing out an oasis", "Trekking onward", "Kicking up sand", 
"Grazing on context",
@@ -133,6 +136,8 @@ class AiPanel {
     // Slash commands
     private final AiSlashCommandRegistry slashCommands = 
AiSlashCommandRegistry.defaults();
     private AiSlashCommandContext slashCommandContext = new 
PanelSlashCommandContext();
+    // auto | core | full, see useCoreTools(); loaded from camel.tui.ai.tools, 
null means auto
+    private volatile String toolMode;
     private final AiCliCommandExecutor cliCommandExecutor = new 
AiCliCommandExecutor();
     private volatile CompletableFuture<AiCliCommandExecutor.Result> 
activeCliCommand;
     private Runnable exitCallback;
@@ -307,6 +312,9 @@ class AiPanel {
                 TuiSettings settings = TuiSettings.load();
                 providerSelector.applyChoice(created, 
settings.getAiProvider(), settings.getAiModel(), settings.getAiUrl());
             }
+            if (toolMode == null) {
+                toolMode = normalizeToolMode(TuiSettings.load().getAiTools());
+            }
             client = created;
             if (!client.detectEndpoint()) {
                 initError
@@ -887,7 +895,7 @@ class AiPanel {
         if (messages == null) {
             messages = new ArrayList<>();
         }
-        messages.add(LlmClient.Message.user(question));
+        messages.add(LlmClient.Message.user(contextualize(question)));
 
         LlmClient.TokenUsage totalUsage = LlmClient.TokenUsage.EMPTY;
         for (int i = 0; i < MAX_ITERATIONS; i++) {
@@ -1661,67 +1669,31 @@ class AiPanel {
                 """;
     }
 
+    /**
+     * The static prefix sent with every request. It deliberately contains 
nothing that changes between turns (the
+     * selected integration travels in the user message instead) so a local 
model's prompt cache can reuse it, and it
+     * does not repeat the tool list because the tool definitions already 
carry every description.
+     */
     private String buildSystemPrompt() {
         StringBuilder sb = new StringBuilder();
         sb.append("You are an Apache Camel assistant running inside the Camel 
TUI terminal console. ");
         sb.append("You help users understand and troubleshoot their running 
Camel integrations.\n\n");
 
-        String selectedName = mcpFacade != null ? 
mcpFacade.getSelectedIntegrationName() : null;
-        String selectedPid = mcpFacade != null ? mcpFacade.getSelectedPid() : 
null;
-        if (selectedName != null && selectedPid != null) {
-            sb.append("The user is monitoring: ").append(selectedName);
-            sb.append(" (PID ").append(selectedPid).append(").\n\n");
-        }
-
-        sb.append("You have tui_* tools to observe and interact with the 
TUI:\n");
-        sb.append("- tui_get_state: see which tab is active and which 
integration is selected\n");
-        sb.append("- tui_get_table: get structured data from any tab WITHOUT 
navigating to it ");
-        sb.append("(Memory, Routes, Endpoints, Health, Process, Threads, 
Metrics, Startup, Heap Histogram, etc.)\n");
-        sb.append("- tui_get_log: read application logs with filtering, 
WITHOUT navigating to the Log tab\n");
-        sb.append("- tui_get_errors: get error details with stack traces, 
WITHOUT navigating to the Errors tab\n");
-        sb.append("- tui_get_diagram: view route diagrams as text, WITHOUT 
navigating to the Diagram tab\n");
-        sb.append("- tui_get_topology: see how routes connect to each other, 
WITHOUT navigating to the Topology tab\n");
-        sb.append("- tui_get_processor_detail: get configured options for all 
processors in a route as structured JSON. ");
-        sb.append("USE THIS to explain what a route does, walk through each 
step, or understand EIP/component configuration. ");
-        sb.append("Set includeDocs=true to get catalog documentation for each 
option\n");
-        sb.append("- tui_catalog_doc: look up Camel catalog documentation for 
any component, EIP, data format, or language\n");
-        sb.append("- tui_get_ai_log: view the AI panel's own activity log 
(questions, tool calls, responses)\n");
-        sb.append("- tui_get_mcp_log: view the MCP server's tool call log 
(external client connections and requests)\n");
-        sb.append("- tui_get_history: trace exchange processing steps, WITHOUT 
navigating to the History tab\n");
-        sb.append("- tui_get_spans: OpenTelemetry span data, WITHOUT 
navigating to the Spans tab\n");
-        sb.append("- tui_navigate: switch tabs, select integrations, select 
routes ");
-        sb.append("- ONLY use when the user explicitly wants to change the 
view\n");
-        sb.append("- tui_control: stop/start routes, restart or stop 
integrations\n");
-        sb.append("- tui_send_message: send test messages to endpoints\n");
-        sb.append("- tui_filter: set or clear text filters on any tab\n");
-        sb.append("- tui_execute_sql: run SQL queries against a DataSource in 
the integration\n");
-        sb.append("- tui_set_log_level: change the runtime log level\n");
-        sb.append("- tui_draw_shape: draw shapes (box, highlight, arrow, 
underline, text) on screen to annotate problems\n");
-        sb.append("- tui_draw_clear: clear drawing overlay\n");
-        sb.append("- tui_locate: find elements on screen by text or diagram 
node ID, returns coordinates for drawing\n");
-        sb.append("- tui_show_caption: display a message to the user on 
screen\n");
-        sb.append("- tui_action: invoke TUI actions (reset-stats, screenshot, 
toggle-theme, etc.)\n");
-        sb.append("- tui_get_themes / tui_set_theme: list and switch TUI 
themes\n");
-        sb.append("- tui_get_files / tui_get_readme: read source files and 
README from integrations\n");
-        sb.append("- tui_update_row: update a database row via 
PreparedStatement\n");
-        sb.append("- tui_set_input: set input field values on tabs 
directly\n");
-        sb.append("- tui_toggle_trace_display: control which sections show in 
History detail view\n");
-        sb.append("- tui_canvas_open / tui_canvas_close: open/close a blank 
canvas for free-form drawing\n");
-        sb.append("- tui_animate: run built-in animations on the canvas\n");
-        sb.append("- tui_send_keys: send key presses to the TUI\n");
-        sb.append("- tui_get_events: see recent user interaction events\n");
-        sb.append("- tui_tape_start / tui_tape_stop: record TUI interactions 
as .tape files\n");
-        sb.append("- tui_wait_for_idle / tui_sleep: timing tools for pacing 
interactions\n\n");
+        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 ");
-        sb.append("- all tui_get_* tools fetch data directly from any tab 
without changing the active tab\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 directly using the tab parameter, 
no navigation needed\n");
-        sb.append("- Use tui_get_state first to understand context before 
acting, if needed\n");
+        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("- 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\n");
-        sb.append("- Use tui_locate + tui_draw_shape to visually highlight 
problems on screen for the user\n");
+        if (!useCoreTools()) {
+            sb.append("- Use tui_locate + tui_draw_shape to visually highlight 
problems on screen for the user\n");
+        }
         if (mcpServerActive) {
             sb.append("\nThe TUI MCP server is available at http://localhost:";)
                     .append(mcpServerPort).append("/mcp for external AI 
agents.");
@@ -1729,12 +1701,54 @@ class AiPanel {
         return sb.toString();
     }
 
+    /**
+     * Prefixes the question with the integration the user is looking at. This 
used to live in the system prompt, but
+     * there it invalidated the model's cached prompt prefix every time the 
selection changed.
+     */
+    private String contextualize(String question) {
+        String selectedName = mcpFacade != null ? 
mcpFacade.getSelectedIntegrationName() : null;
+        String selectedPid = mcpFacade != null ? mcpFacade.getSelectedPid() : 
null;
+        if (selectedName != null && selectedPid != null) {
+            return "[Monitoring " + selectedName + " (PID " + selectedPid + 
")]\n" + question;
+        }
+        return question;
+    }
+
+    /**
+     * Whether only the {@link TuiToolRegistry#CORE_TOOLS} are sent: always in 
{@code core} mode, never in {@code full}
+     * mode, and for local providers in {@code auto} mode.
+     */
+    private boolean useCoreTools() {
+        String mode = toolMode == null ? TOOL_MODE_AUTO : toolMode;
+        if (TOOL_MODE_CORE.equals(mode)) {
+            return true;
+        }
+        if (TOOL_MODE_FULL.equals(mode)) {
+            return false;
+        }
+        return client != null && client.isLocalEndpoint();
+    }
+
+    private String describeToolMode() {
+        if (toolRegistry == null) {
+            return "no tools available";
+        }
+        int total = toolRegistry.getToolDefinitions().size();
+        int active = useCoreTools() ? 
toolRegistry.getCoreToolDefinitions().size() : total;
+        String mode = toolMode == null ? TOOL_MODE_AUTO : toolMode;
+        String detail = TOOL_MODE_AUTO.equals(mode)
+                ? (useCoreTools() ? " (local provider)" : " (hosted 
provider)") : "";
+        return (useCoreTools() ? "core" : "full") + " (" + active + " of " + 
total + " tools), mode " + mode + detail;
+    }
+
     private List<LlmClient.ToolDef> buildTuiToolDefinitions() {
         if (toolRegistry == null) {
             return List.of();
         }
         List<LlmClient.ToolDef> defs = new ArrayList<>();
-        for (TuiToolRegistry.ToolDef td : toolRegistry.getToolDefinitions()) {
+        List<TuiToolRegistry.ToolDef> source
+                = useCoreTools() ? toolRegistry.getCoreToolDefinitions() : 
toolRegistry.getToolDefinitions();
+        for (TuiToolRegistry.ToolDef td : source) {
             defs.add(new LlmClient.ToolDef(td.name(), td.description(), 
td.inputSchema()));
         }
         return defs;
@@ -1841,6 +1855,41 @@ class AiPanel {
         return inputBuffer.toString();
     }
 
+    /**
+     * Maps a configured tool mode to {@code auto}, {@code core} or {@code 
full}; blank means {@code auto}, anything
+     * else is rejected with {@code null}.
+     */
+    static String normalizeToolMode(String mode) {
+        if (mode == null || mode.isBlank()) {
+            return TOOL_MODE_AUTO;
+        }
+        String value = mode.trim().toLowerCase();
+        return switch (value) {
+            case TOOL_MODE_AUTO, TOOL_MODE_CORE, TOOL_MODE_FULL -> value;
+            default -> null;
+        };
+    }
+
+    void setToolRegistryForTesting(TuiToolRegistry registry) {
+        this.toolRegistry = registry;
+    }
+
+    void setToolModeForTesting(String mode) {
+        this.toolMode = normalizeToolMode(mode);
+    }
+
+    String systemPromptForTesting() {
+        return buildSystemPrompt();
+    }
+
+    List<LlmClient.ToolDef> toolDefinitionsForTesting() {
+        return buildTuiToolDefinitions();
+    }
+
+    String describeToolModeForTesting() {
+        return describeToolMode();
+    }
+
     void setExitCallbackForTestingOrRuntime(Runnable callback) {
         this.exitCallback = callback;
     }
@@ -1918,6 +1967,24 @@ class AiPanel {
             return ctx != null ? ctx.selectedName() : null;
         }
 
+        @Override
+        public String describeToolMode() {
+            return AiPanel.this.describeToolMode();
+        }
+
+        @Override
+        public boolean switchToolMode(String mode) {
+            String normalized = normalizeToolMode(mode);
+            if (normalized == null) {
+                return false;
+            }
+            toolMode = normalized;
+            TuiSettings settings = TuiSettings.load();
+            settings.setAiTools(TOOL_MODE_AUTO.equals(normalized) ? null : 
normalized);
+            settings.save();
+            return true;
+        }
+
         @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 98e6b7adee33..3afa1cc4b74b 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
@@ -42,6 +42,17 @@ interface AiSlashCommandContext {
 
     String selectedProcessName();
 
+    /**
+     * Describes the tool set currently sent to the model, for example {@code 
core (18 of 46 tools), mode auto}.
+     */
+    String describeToolMode();
+
+    /**
+     * 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.
+     */
+    boolean switchToolMode(String mode);
+
     CompletableFuture<AiCliCommandExecutor.Result> 
executeCli(AiCliCommandExecutor.Request request);
 
     void cancelCli();
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 12b1a3f6481d..9b4f580c83fd 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
@@ -68,6 +68,9 @@ final class AiSlashCommandRegistry {
         commands.add(new Descriptor(
                 "model", List.of("m"), "Show or switch the AI model", 
"<model>",
                 AiSlashCommandRegistry::executeModel));
+        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(
                 "clear", List.of("c"), "Clear the conversation", null,
                 (context, arguments) -> {
@@ -296,6 +299,17 @@ final class AiSlashCommandRegistry {
         return CommandResult.listModels();
     }
 
+    private static CommandResult executeTools(AiSlashCommandContext context, 
String arguments) {
+        if (arguments.isBlank()) {
+            return CommandResult.system("Tool set: " + 
context.describeToolMode());
+        }
+        String mode = arguments.trim().toLowerCase();
+        if (!context.switchToolMode(mode)) {
+            return CommandResult.error("Unknown tool mode '" + 
arguments.trim() + "'. Use auto, core or full.");
+        }
+        return CommandResult.system("Tool set: " + context.describeToolMode());
+    }
+
     private static int firstWhitespace(String value) {
         for (int i = 0; i < value.length(); i++) {
             if (Character.isWhitespace(value.charAt(i))) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
index 542aba850790..c6ababc82217 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -39,6 +39,7 @@ final class TuiSettings {
     static final String PROP_AI_PROVIDER = "camel.tui.ai.provider";
     static final String PROP_AI_MODEL = "camel.tui.ai.model";
     static final String PROP_AI_URL = "camel.tui.ai.url";
+    static final String PROP_AI_TOOLS = "camel.tui.ai.tools";
     static final String PROP_PROXY_HOST = "camel.tui.proxyHost";
     static final String PROP_PROXY_PORT = "camel.tui.proxyPort";
     static final String PROP_SHELL_HISTORY = "camel.tui.shell.history";
@@ -59,6 +60,7 @@ final class TuiSettings {
     private String aiProvider;
     private String aiModel;
     private String aiUrl;
+    private String aiTools;
     private String shellHistory;
     private String aiPromptHistory;
     private String confirmActions;
@@ -154,6 +156,18 @@ final class TuiSettings {
         this.aiUrl = aiUrl;
     }
 
+    /**
+     * Which tui_* tools the AI panel sends to the model: {@code auto} 
(default: the core set for local providers, all
+     * tools otherwise), {@code core} or {@code full}.
+     */
+    String getAiTools() {
+        return aiTools;
+    }
+
+    void setAiTools(String aiTools) {
+        this.aiTools = aiTools;
+    }
+
     String getShellHistory() {
         return shellHistory;
     }
@@ -247,6 +261,7 @@ final class TuiSettings {
             settings.aiProvider = 
trimToNull(TuiUserConfig.read(PROP_AI_PROVIDER));
             settings.aiModel = trimToNull(TuiUserConfig.read(PROP_AI_MODEL));
             settings.aiUrl = trimToNull(TuiUserConfig.read(PROP_AI_URL));
+            settings.aiTools = trimToNull(TuiUserConfig.read(PROP_AI_TOOLS));
             settings.shellHistory = 
trimToNull(TuiUserConfig.read(PROP_SHELL_HISTORY));
             settings.aiPromptHistory = 
trimToNull(TuiUserConfig.read(PROP_AI_PROMPT_HISTORY));
             settings.confirmActions = 
trimToNull(TuiUserConfig.read(PROP_CONFIRM_ACTIONS));
@@ -277,6 +292,7 @@ final class TuiSettings {
             TuiUserConfig.write(PROP_AI_PROVIDER, aiProvider);
             TuiUserConfig.write(PROP_AI_MODEL, aiModel);
             TuiUserConfig.write(PROP_AI_URL, aiUrl);
+            TuiUserConfig.write(PROP_AI_TOOLS, aiTools);
             TuiUserConfig.write(PROP_SHELL_HISTORY, shellHistory);
             TuiUserConfig.write(PROP_AI_PROMPT_HISTORY, aiPromptHistory);
             TuiUserConfig.write(PROP_CONFIRM_ACTIONS, confirmActions);
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 c1263c518536..03bbb0fc4caf 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
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import dev.tamboui.buffer.Buffer;
@@ -79,7 +80,19 @@ class TuiToolRegistry {
     }
 
     /**
-     * Returns all 42 tool definitions. The result is cached since it is 
immutable.
+     * The tools needed to answer questions and troubleshoot from the built-in 
AI panel. The remaining tools drive the
+     * screen (drawing, animation, key presses, tape recording, themes) and 
exist for external MCP agents. Every tool
+     * schema is sent on every request, and a local model pays for that in 
prompt-processing time, so the AI panel sends
+     * only this subset to local providers unless configured otherwise.
+     */
+    static final Set<String> CORE_TOOLS = Set.of(
+            "tui_get_state", "tui_get_options", "tui_get_table", 
"tui_get_log", "tui_get_errors",
+            "tui_get_diagram", "tui_get_topology", "tui_get_processor_detail", 
"tui_catalog_doc",
+            "tui_get_history", "tui_get_spans", "tui_control", 
"tui_send_message", "tui_get_files",
+            "tui_get_readme", "tui_navigate", "tui_set_log_level", 
"tui_filter");
+
+    /**
+     * Returns all tool definitions. The result is cached since it is 
immutable.
      */
     List<ToolDef> getToolDefinitions() {
         List<ToolDef> tools = cachedTools;
@@ -91,6 +104,13 @@ class TuiToolRegistry {
         return tools;
     }
 
+    /**
+     * Returns only the {@link #CORE_TOOLS} definitions, in registry order.
+     */
+    List<ToolDef> getCoreToolDefinitions() {
+        return getToolDefinitions().stream().filter(t -> 
CORE_TOOLS.contains(t.name())).toList();
+    }
+
     /**
      * Executes a tool by name, returns result string.
      */
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 41dee94aa795..9da0a15db472 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
@@ -728,6 +728,67 @@ class AiPanelTest {
         assertEquals("/clear-history", panel.inputBufferForTesting());
     }
 
+    // ---- tool set and system prompt tests ----
+
+    @Test
+    void localProviderGetsCoreToolsAndHostedProviderGetsAll() {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        RecordingLlmClient client = new RecordingLlmClient("ok");
+        panel.setClientForTesting(client);
+
+        // auto mode: a hosted provider gets every tool
+        assertEquals(new TuiToolRegistry(null).getToolDefinitions().size(), 
panel.toolDefinitionsForTesting().size());
+        assertTrue(panel.systemPromptForTesting().contains("tui_draw_shape"));
+
+        // auto mode: a local provider only gets the core set, and the prompt 
no longer suggests drawing tools
+        client.withApiType(LlmClient.ApiType.ollama);
+        assertEquals(TuiToolRegistry.CORE_TOOLS.size(), 
panel.toolDefinitionsForTesting().size());
+        assertTrue(panel.toolDefinitionsForTesting().stream()
+                .allMatch(def -> 
TuiToolRegistry.CORE_TOOLS.contains(def.name())));
+        assertFalse(panel.systemPromptForTesting().contains("tui_draw_shape"));
+        assertTrue(panel.describeToolModeForTesting().startsWith("core (18 of 
"));
+    }
+
+    @Test
+    void explicitToolModeOverridesProviderDetection() {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        RecordingLlmClient client = new RecordingLlmClient("ok");
+        client.withApiType(LlmClient.ApiType.ollama);
+        panel.setClientForTesting(client);
+
+        panel.setToolModeForTesting("full");
+        assertEquals(new TuiToolRegistry(null).getToolDefinitions().size(), 
panel.toolDefinitionsForTesting().size());
+
+        panel.setToolModeForTesting("core");
+        client.withApiType(LlmClient.ApiType.openai);
+        assertEquals(TuiToolRegistry.CORE_TOOLS.size(), 
panel.toolDefinitionsForTesting().size());
+    }
+
+    @Test
+    void systemPromptIsStableAndDoesNotRepeatTheToolList() {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        panel.setClientForTesting(new RecordingLlmClient("ok"));
+
+        String prompt = panel.systemPromptForTesting();
+
+        // the tool definitions already describe every tool, so the prompt 
must not list them again
+        assertFalse(prompt.contains("- tui_get_table:"));
+        assertFalse(prompt.contains("The user is monitoring"));
+        assertEquals(prompt, panel.systemPromptForTesting());
+    }
+
+    @Test
+    void normalizeToolModeAcceptsKnownValuesOnly() {
+        assertEquals("auto", AiPanel.normalizeToolMode(null));
+        assertEquals("auto", AiPanel.normalizeToolMode("  "));
+        assertEquals("core", AiPanel.normalizeToolMode("Core"));
+        assertEquals("full", AiPanel.normalizeToolMode("FULL"));
+        assertNull(AiPanel.normalizeToolMode("bogus"));
+    }
+
     // ---- paste tests ----
 
     @Test
@@ -853,6 +914,16 @@ class AiPanelTest {
 
     static final class FakeSlashContext implements AiSlashCommandContext {
 
+        @Override
+        public String describeToolMode() {
+            return "full (46 of 46 tools), mode auto";
+        }
+
+        @Override
+        public boolean switchToolMode(String mode) {
+            return true;
+        }
+
         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 37f18f37375f..30ecddcfaba9 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
@@ -33,7 +33,9 @@ class AiSlashCommandRegistryTest {
     void descriptorsKeepStableOrder() {
         AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
 
-        assertEquals(List.of("help", "provider", "model", "clear", 
"clear-history", "close", "quit", "run", "infra", "send"),
+        assertEquals(
+                List.of("help", "provider", "model", "tools", "clear", 
"clear-history", "close", "quit", "run", "infra",
+                        "send"),
                 
registry.descriptors().stream().map(AiSlashCommandRegistry.Descriptor::name).toList());
     }
 
@@ -73,7 +75,7 @@ class AiSlashCommandRegistryTest {
     void completionsIncludeAllCommandsForBareSlash() {
         AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
 
-        assertEquals(10, registry.completionsFor("/").size());
+        assertEquals(11, registry.completionsFor("/").size());
         assertFalse(registry.completionsFor("/").stream().anyMatch(descriptor 
-> "exit".equals(descriptor.name())));
     }
 
@@ -272,8 +274,71 @@ class AiSlashCommandRegistryTest {
         }
     }
 
+    @Test
+    void toolsWithoutArgumentsDescribesCurrentToolSet() {
+        AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
+        ToolModeContext context = new ToolModeContext();
+
+        AiSlashCommandRegistry.CommandResult result = 
registry.execute("/tools", context);
+
+        assertEquals("Tool set: core (18 of 46 tools), mode auto", 
result.text());
+        assertNull(context.switchedTo);
+    }
+
+    @Test
+    void toolsSwitchesModeCaseInsensitively() {
+        AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
+        ToolModeContext context = new ToolModeContext();
+
+        AiSlashCommandRegistry.CommandResult result = registry.execute("/t 
Full", context);
+
+        assertEquals("full", context.switchedTo);
+        assertEquals(AiRole.SYSTEM, result.role());
+    }
+
+    @Test
+    void toolsRejectsUnknownMode() {
+        AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
+        ToolModeContext context = new ToolModeContext();
+
+        AiSlashCommandRegistry.CommandResult result = registry.execute("/tools 
bogus", context);
+
+        assertEquals(AiRole.ERROR, result.role());
+        assertTrue(result.text().contains("bogus"));
+        assertNull(context.switchedTo);
+    }
+
+    private static final class ToolModeContext extends NoopSlashContext {
+
+        private String switchedTo;
+
+        @Override
+        public String describeToolMode() {
+            return "core (18 of 46 tools), mode auto";
+        }
+
+        @Override
+        public boolean switchToolMode(String mode) {
+            if (!List.of("auto", "core", "full").contains(mode)) {
+                return false;
+            }
+            switchedTo = mode;
+            return true;
+        }
+    }
+
     private static class NoopSlashContext implements AiSlashCommandContext {
 
+        @Override
+        public String describeToolMode() {
+            return "";
+        }
+
+        @Override
+        public boolean switchToolMode(String mode) {
+            return false;
+        }
+
         @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/TuiSettingsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
index 53da3f8a7649..00ba4ad323fe 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
@@ -63,6 +63,7 @@ class TuiSettingsTest {
         settings.setAiProvider("gemini");
         settings.setAiModel("gemini-3.5-flash");
         settings.setAiUrl("https://generativelanguage.googleapis.com";);
+        settings.setAiTools("core");
         settings.setShellHistory("25");
         settings.setAiPromptHistory("50");
         settings.setPanelPosition("top");
@@ -76,6 +77,7 @@ class TuiSettingsTest {
         assertThat(loaded.getAiProvider()).isEqualTo("gemini");
         assertThat(loaded.getAiModel()).isEqualTo("gemini-3.5-flash");
         
assertThat(loaded.getAiUrl()).isEqualTo("https://generativelanguage.googleapis.com";);
+        assertThat(loaded.getAiTools()).isEqualTo("core");
         assertThat(loaded.getShellHistory()).isEqualTo("25");
         assertThat(loaded.getAiPromptHistory()).isEqualTo("50");
         assertThat(loaded.getPanelPosition()).isEqualTo("top");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryCoreToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryCoreToolsTest.java
new file mode 100644
index 000000000000..b38d749ce48f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistryCoreToolsTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.Set;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TuiToolRegistryCoreToolsTest {
+
+    @Test
+    void everyCoreToolExistsInTheRegistry() {
+        TuiToolRegistry registry = new TuiToolRegistry(null);
+        Set<String> all = registry.getToolDefinitions().stream()
+                
.map(TuiToolRegistry.ToolDef::name).collect(Collectors.toSet());
+
+        assertTrue(all.containsAll(TuiToolRegistry.CORE_TOOLS),
+                "core tools missing from registry: " + 
TuiToolRegistry.CORE_TOOLS.stream()
+                        .filter(name -> !all.contains(name)).toList());
+    }
+
+    @Test
+    void coreDefinitionsAreTheCoreSubsetInRegistryOrder() {
+        TuiToolRegistry registry = new TuiToolRegistry(null);
+        List<TuiToolRegistry.ToolDef> core = registry.getCoreToolDefinitions();
+        List<TuiToolRegistry.ToolDef> all = registry.getToolDefinitions();
+
+        assertEquals(TuiToolRegistry.CORE_TOOLS.size(), core.size());
+        assertTrue(core.size() < all.size());
+        assertTrue(core.stream().allMatch(def -> 
TuiToolRegistry.CORE_TOOLS.contains(def.name())));
+        assertEquals(all.stream().filter(def -> 
TuiToolRegistry.CORE_TOOLS.contains(def.name())).toList(), core);
+    }
+
+    @Test
+    void coreSetLeavesOutScreenAutomationTools() {
+        Set<String> automation = Set.of("tui_draw", "tui_draw_shape", 
"tui_animate", "tui_send_keys", "tui_sleep",
+                "tui_tape_start", "tui_canvas_open", "tui_set_theme");
+
+        
assertFalse(TuiToolRegistry.CORE_TOOLS.stream().anyMatch(automation::contains));
+    }
+}

Reply via email to