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

davsclaus pushed a commit to branch fix/CAMEL-23855
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d6ce487c08f5193ff04af96dfd49b44a22dc56c7
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 29 14:48:18 2026 +0200

    CAMEL-23855: Add F8 AI prompt panel to TUI
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/dsl/jbang/core/commands/Ask.java  | 836 +--------------------
 .../core/commands/{Ask.java => AskTools.java}      | 485 +++---------
 .../camel/dsl/jbang/core/commands/LlmClient.java   |  64 +-
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 406 ++++++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  39 +-
 5 files changed, 578 insertions(+), 1252 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
index 9a7e5af16761..f6a3ad5d70ba 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
@@ -16,30 +16,13 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
-import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
-import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
-import org.apache.camel.tooling.model.ComponentModel;
-import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.json.JsonObject;
-import org.apache.camel.util.json.Jsoner;
 import org.jline.reader.EndOfFileException;
 import org.jline.reader.LineReader;
 import org.jline.reader.LineReaderBuilder;
@@ -113,8 +96,7 @@ public class Ask extends CamelCommand {
     boolean verbose;
 
     private long targetPid;
-    private CamelCatalog catalog;
-    private volatile List<JsonObject> commandMetadataCache;
+    private AskTools askTools;
 
     public Ask(CamelJBangMain main) {
         super(main);
@@ -149,8 +131,9 @@ public class Ask extends CamelCommand {
             targetPid = -1;
         }
 
+        askTools = new AskTools(targetPid);
         String systemPrompt = buildSystemPrompt(process);
-        List<LlmClient.ToolDef> tools = buildToolDefinitions();
+        List<LlmClient.ToolDef> tools = askTools.buildToolDefinitions();
 
         if (question == null || question.isEmpty()) {
             return runInteractiveChat(client, process, systemPrompt, tools);
@@ -227,7 +210,7 @@ public class Ask extends CamelCommand {
                     if (showTools) {
                         printer().println("[tool] " + toolCall.name() + "(" + 
toolCall.arguments().toJson() + ")");
                     }
-                    String result = executeTool(toolCall.name(), 
toolCall.arguments());
+                    String result = askTools.executeTool(toolCall.name(), 
toolCall.arguments());
                     if (showTools) {
                         printer().println("[result] " + truncate(result, 200));
                     }
@@ -283,570 +266,12 @@ public class Ask extends CamelCommand {
     // ---- System prompt ----
 
     private String buildSystemPrompt(RuntimeHelper.ProcessInfo process) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("You are an Apache Camel assistant. ");
-        sb.append("You help users build, understand, and troubleshoot Camel 
applications.\n\n");
-
-        if (process != null) {
-            sb.append("You are connected to a running Camel application: ");
-            sb.append(process.name()).append(" (PID 
").append(process.pid()).append("). ");
-            sb.append("Use the runtime inspection tools to gather information 
about it.\n\n");
-        } else {
-            List<RuntimeHelper.ProcessInfo> available = 
RuntimeHelper.discoverProcesses();
-            if (!available.isEmpty()) {
-                sb.append("No Camel process is currently selected. ");
-                sb.append("Use list_processes to see available processes, then 
select_process to connect to one. ");
-                sb.append("Runtime inspection tools will not work until a 
process is selected.\n\n");
-            }
-        }
-
-        sb.append("You can search the Camel catalog (components, EIPs), browse 
examples, ");
-        sb.append("read/write files, and execute any Camel CLI command.\n\n");
-        sb.append("For CLI commands beyond the built-in tools, use 
cli_list_commands to discover ");
-        sb.append("available commands, cli_command_help to see options, and 
cli_exec to run them.\n\n");
-        sb.append("Guidelines:\n");
-        sb.append("- When creating routes, use YAML DSL format (Camel's 
recommended format for JBang)\n");
-        sb.append("- Look at existing files first with list_files/read_file 
before creating new ones\n");
-        sb.append("- Use catalog tools to look up component syntax before 
writing routes\n");
-        sb.append("- Use examples as reference when building new routes\n");
-        sb.append("- Be concise and actionable in your answers\n");
-        sb.append("- Format output as plain text for terminal display, do not 
use markdown\n");
-        if (process != null) {
-            sb.append("- Start by gathering relevant information using the 
available runtime tools\n");
-            sb.append("- If something looks wrong, explain what it means and 
suggest fixes\n");
-            sb.append("- To stop routes or the application, always use the 
provided tools ");
-            sb.append("(stop_route, stop_application) for graceful shutdown. 
Never suggest kill or kill -9.\n");
-        }
-        return sb.toString();
-    }
-
-    // ---- Tool definitions ----
-
-    private List<LlmClient.ToolDef> buildToolDefinitions() {
-        List<LlmClient.ToolDef> tools = new ArrayList<>();
-
-        // Process discovery and selection
-        tools.add(new LlmClient.ToolDef(
-                "list_processes",
-                "List all running Camel processes with their PID and name. Use 
this to discover available processes before selecting one.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "select_process",
-                "Select a running Camel process by name or PID to inspect. 
Required when multiple processes are running. After selection, all runtime 
tools (get_routes, get_context, etc.) will target this process.",
-                objectParams(Map.of(
-                        "name", stringProp("Name or PID of the Camel process 
to connect to")))));
-
-        // Status-file tools (no parameters needed)
-        tools.add(new LlmClient.ToolDef(
-                "get_context",
-                "Get Camel context info: name, version, state, uptime, route 
count, exchange statistics.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_routes",
-                "List all routes with their state, uptime, messages processed, 
last error, and throughput.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_health",
-                "Get health check status for the Camel application.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_endpoints",
-                "List all endpoints registered in the Camel context with URIs 
and usage stats.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_inflight",
-                "Show currently in-flight exchanges (messages being 
processed).",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_blocked",
-                "Show blocked exchanges that are stuck or waiting.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_consumers",
-                "Show consumer statistics (polling and event-driven 
consumers).",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "get_properties",
-                "Show configuration properties of the running Camel 
application.",
-                emptyParams()));
-
-        // IPC action tools (with parameters)
-        tools.add(new LlmClient.ToolDef(
-                "get_route_source",
-                "Get the source code of routes. Use filter to limit by 
filename (supports wildcards).",
-                objectParams(Map.of(
-                        "filter", stringProp("Filter source files by name 
(supports wildcards). Use * for all.")))));
-        tools.add(new LlmClient.ToolDef(
-                "get_route_dump",
-                "Dump route definitions in XML or YAML format.",
-                objectParams(Map.of(
-                        "routeId", stringProp("Route ID to dump (use * for all 
routes)"),
-                        "format", stringProp("Output format: xml or yaml 
(default: yaml)")))));
-        tools.add(new LlmClient.ToolDef(
-                "get_route_structure",
-                "Show the route structure as a tree of processors.",
-                objectParams(Map.of(
-                        "routeId", stringProp("Route ID to inspect (use * for 
all routes)")))));
-        tools.add(new LlmClient.ToolDef(
-                "get_top_processors",
-                "Show top processor statistics: which processors are slowest 
and most active.",
-                emptyParams()));
-        tools.add(new LlmClient.ToolDef(
-                "trace_control",
-                "Enable, disable, or dump message tracing.",
-                objectParams(Map.of(
-                        "action", stringProp("Action: enable, disable, or 
dump")))));
-
-        // Route lifecycle tools
-        tools.add(new LlmClient.ToolDef(
-                "stop_route",
-                "Gracefully stop a route. The route will finish processing 
in-flight exchanges before stopping.",
-                objectParams(Map.of(
-                        "routeId", stringProp("The ID of the route to 
stop")))));
-        tools.add(new LlmClient.ToolDef(
-                "start_route",
-                "Start a stopped route.",
-                objectParams(Map.of(
-                        "routeId", stringProp("The ID of the route to 
start")))));
-        tools.add(new LlmClient.ToolDef(
-                "suspend_route",
-                "Suspend a route (pauses the consumer but keeps the route 
loaded).",
-                objectParams(Map.of(
-                        "routeId", stringProp("The ID of the route to 
suspend")))));
-        tools.add(new LlmClient.ToolDef(
-                "resume_route",
-                "Resume a suspended route.",
-                objectParams(Map.of(
-                        "routeId", stringProp("The ID of the route to 
resume")))));
-
-        // Application lifecycle
-        tools.add(new LlmClient.ToolDef(
-                "stop_application",
-                "Gracefully stop the Camel application. The application will 
finish processing in-flight exchanges and shut down cleanly. Use this instead 
of kill.",
-                emptyParams()));
-
-        // Catalog tools
-        tools.add(new LlmClient.ToolDef(
-                "catalog_components",
-                "Search the Camel component catalog by name or label. Returns 
component name, title, description, and labels.",
-                objectParams(Map.of(
-                        "filter", stringProp("Filter by name, title, or 
description (case-insensitive substring)"),
-                        "label", stringProp("Filter by category label (e.g., 
cloud, messaging, database, file)")))));
-        tools.add(new LlmClient.ToolDef(
-                "catalog_component_doc",
-                "Get detailed documentation for a Camel component: URI syntax 
and endpoint options.",
-                objectParams(Map.of(
-                        "component", stringProp("Component name (e.g., kafka, 
http, file, timer)")))));
-        tools.add(new LlmClient.ToolDef(
-                "catalog_eips",
-                "Search EIPs (Enterprise Integration Patterns) like split, 
aggregate, filter, choice, multicast.",
-                objectParams(Map.of(
-                        "filter", stringProp("Filter by name, title, or 
description (case-insensitive substring)")))));
-
-        // Example tools
-        tools.add(new LlmClient.ToolDef(
-                "list_examples",
-                "List available Camel CLI examples. Returns name, title, 
description, difficulty level, and tags.",
-                objectParams(Map.of(
-                        "filter", stringProp("Filter by name, description, or 
tag (case-insensitive)"),
-                        "level", stringProp("Filter by difficulty: beginner, 
intermediate, or advanced")))));
-        tools.add(new LlmClient.ToolDef(
-                "get_example_file",
-                "Get the content of a file from a bundled Camel CLI example. 
Use list_examples first to find available examples.",
-                objectParams(Map.of(
-                        "example", stringProp("Example name (e.g., timer-log, 
rest-api, circuit-breaker)"),
-                        "file", stringProp("File name within the example 
(e.g., route.camel.yaml)")))));
-
-        // CLI tools (access to all camel CLI commands)
-        tools.add(new LlmClient.ToolDef(
-                "cli_list_commands",
-                "List available Camel CLI commands. Returns command names and 
descriptions. Use filter to narrow results.",
-                objectParams(Map.of(
-                        "filter", stringProp("Filter by command name or 
description (case-insensitive substring)")))));
-        tools.add(new LlmClient.ToolDef(
-                "cli_command_help",
-                "Get detailed help for a specific Camel CLI command, including 
all options with types and defaults.",
-                objectParams(Map.of(
-                        "command", stringProp("Full command name (e.g., 'get 
error', 'catalog component', 'run')")))));
-        tools.add(new LlmClient.ToolDef(
-                "cli_exec",
-                "Execute any Camel CLI command and return its output. Use 
cli_list_commands and cli_command_help first to discover commands and their 
options. CAUTION: some commands (stop, cmd stop-route, cmd stop-group) are 
destructive and will affect running integrations. Always confirm with the user 
before executing destructive commands.",
-                objectParams(Map.of(
-                        "command", stringProp(
-                                "The full command line to execute (e.g., 'get 
error --diagram', 'catalog component --filter=kafka')")))));
-
-        // File tools
-        tools.add(new LlmClient.ToolDef(
-                "list_files",
-                "List files in a directory (up to 2 levels deep). Defaults to 
current working directory.",
-                objectParams(Map.of(
-                        "path", stringProp("Directory path relative to CWD 
(default: current directory)")))));
-        tools.add(new LlmClient.ToolDef(
-                "read_file",
-                "Read the content of a file. Useful for inspecting route 
definitions, configuration, and properties files.",
-                objectParams(Map.of(
-                        "file", stringProp("File path relative to CWD")))));
-        tools.add(new LlmClient.ToolDef(
-                "write_file",
-                "Write content to a file. Creates parent directories if 
needed. Use for creating or editing route definitions and configuration files.",
-                objectParams(Map.of(
-                        "file", stringProp("File path relative to CWD"),
-                        "content", stringProp("The content to write to the 
file")))));
-
-        return tools;
-    }
-
-    // ---- Tool execution ----
-
-    private String executeTool(String name, JsonObject args) {
-        try {
-            return switch (name) {
-                // Runtime tools (require a running process)
-                case "list_processes" -> executeListProcesses();
-                case "select_process" -> executeSelectProcess(args);
-                case "get_context" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "context");
-                case "get_routes" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "routes");
-                case "get_health" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "healthChecks");
-                case "get_endpoints" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "endpoints");
-                case "get_inflight" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "inflight");
-                case "get_blocked" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "blocked");
-                case "get_consumers" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "consumers");
-                case "get_properties" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "properties");
-                case "get_route_source" -> targetPid < 0 ? NO_PROCESS : 
executeRouteSource(args);
-                case "get_route_dump" -> targetPid < 0 ? NO_PROCESS : 
executeRouteDump(args);
-                case "get_route_structure" -> targetPid < 0 ? NO_PROCESS : 
executeRouteStructure(args);
-                case "get_top_processors" ->
-                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.executeAction(targetPid, "top-processors", null);
-                case "trace_control" -> targetPid < 0 ? NO_PROCESS : 
executeTraceControl(args);
-                case "stop_route" -> targetPid < 0 ? NO_PROCESS : 
executeRouteCommand(args, "stop");
-                case "start_route" -> targetPid < 0 ? NO_PROCESS : 
executeRouteCommand(args, "start");
-                case "suspend_route" -> targetPid < 0 ? NO_PROCESS : 
executeRouteCommand(args, "suspend");
-                case "resume_route" -> targetPid < 0 ? NO_PROCESS : 
executeRouteCommand(args, "resume");
-                case "stop_application" -> targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.stopApplication(targetPid);
-                // Catalog tools
-                case "catalog_components" -> executeCatalogComponents(args);
-                case "catalog_component_doc" -> 
executeCatalogComponentDoc(args);
-                case "catalog_eips" -> executeCatalogEips(args);
-                // Example tools
-                case "list_examples" -> executeListExamples(args);
-                case "get_example_file" -> executeGetExampleFile(args);
-                // CLI tools
-                case "cli_list_commands" -> executeCliListCommands(args);
-                case "cli_command_help" -> executeCliCommandHelp(args);
-                case "cli_exec" -> executeCliExec(args);
-                // File tools
-                case "list_files" -> executeListFiles(args);
-                case "read_file" -> executeReadFile(args);
-                case "write_file" -> executeWriteFile(args);
-                default -> "Unknown tool: " + name;
-            };
-        } catch (Exception e) {
-            return "Error executing " + name + ": " + e.getMessage();
-        }
-    }
-
-    private String executeListProcesses() {
-        List<RuntimeHelper.ProcessInfo> processes = 
RuntimeHelper.discoverProcesses();
-        if (processes.isEmpty()) {
-            return "No running Camel processes found. Start one with: camel 
run <file>";
-        }
-        JsonObject response = new JsonObject();
-        response.put("count", processes.size());
-        List<JsonObject> list = new ArrayList<>();
-        for (RuntimeHelper.ProcessInfo p : processes) {
-            JsonObject entry = new JsonObject();
-            entry.put("pid", p.pid());
-            entry.put("name", p.name());
-            entry.put("selected", p.pid() == targetPid);
-            list.add(entry);
-        }
-        response.put("processes", list);
-        if (targetPid < 0) {
-            response.put("hint", "No process selected. Use select_process to 
connect to one.");
-        }
-        return response.toJson();
-    }
-
-    private String executeSelectProcess(JsonObject args) {
-        String name = args.getString("name");
-        if (name == null || name.isBlank()) {
-            return "Error: name or PID is required";
-        }
-        RuntimeHelper.ProcessInfo found = RuntimeHelper.findProcess(name);
-        if (found == null) {
-            List<RuntimeHelper.ProcessInfo> processes = 
RuntimeHelper.discoverProcesses();
-            if (processes.isEmpty()) {
-                return "No running Camel processes found.";
-            }
-            StringBuilder sb = new StringBuilder("No process found matching: " 
+ name + ". Available:\n");
-            processes.forEach(p -> sb.append("  ").append(p.name()).append(" 
(PID ").append(p.pid()).append(")\n"));
-            return sb.toString();
-        }
-        targetPid = found.pid();
-        return "Connected to " + found.name() + " (PID " + found.pid() + "). 
Runtime tools are now active.";
+        return AskTools.buildSystemPrompt(
+                process != null ? process.pid() : -1,
+                process != null ? process.name() : null);
     }
 
-    private String executeRouteSource(JsonObject args) {
-        String filter = args.getString("filter");
-        return RuntimeHelper.executeAction(targetPid, "source",
-                root -> root.put("filter", filter != null ? filter : "*"));
-    }
-
-    private String executeRouteDump(JsonObject args) {
-        String routeId = args.getString("routeId");
-        String format = args.getString("format");
-        return RuntimeHelper.executeAction(targetPid, "route-dump", root -> {
-            root.put("id", routeId != null ? routeId : "*");
-            root.put("format", format != null ? format : "yaml");
-        });
-    }
-
-    private String executeRouteStructure(JsonObject args) {
-        String routeId = args.getString("routeId");
-        return RuntimeHelper.executeAction(targetPid, "route-structure",
-                root -> root.put("id", routeId != null ? routeId : "*"));
-    }
-
-    private String executeTraceControl(JsonObject args) {
-        String action = args.getString("action");
-        if (action == null) {
-            return "Error: action is required (enable, disable, dump)";
-        }
-        return RuntimeHelper.executeAction(targetPid, "trace", root -> {
-            switch (action.toLowerCase()) {
-                case "enable" -> root.put("enabled", "true");
-                case "disable" -> root.put("enabled", "false");
-                case "dump" -> root.put("dump", "true");
-                default -> root.put("enabled", action);
-            }
-        });
-    }
-
-    private String executeRouteCommand(JsonObject args, String command) {
-        String routeId = args.getString("routeId");
-        if (routeId == null || routeId.isBlank()) {
-            return "Error: routeId is required";
-        }
-        return RuntimeHelper.executeAction(targetPid, "route", root -> {
-            root.put("id", routeId);
-            root.put("command", command);
-        });
-    }
-
-    // ---- Catalog tools ----
-
-    private String executeCatalogComponents(JsonObject args) {
-        String filter = args.getString("filter");
-        String label = args.getString("label");
-        CamelCatalog catalog = getCatalog();
-
-        List<JsonObject> results = catalog.findComponentNames().stream()
-                .map(catalog::componentModel)
-                .filter(m -> m != null)
-                .filter(m -> matchesFilter(m.getScheme(), m.getTitle(), 
m.getDescription(), filter))
-                .filter(m -> label == null || label.isBlank()
-                        || (m.getLabel() != null && 
m.getLabel().toLowerCase().contains(label.toLowerCase())))
-                .limit(20)
-                .map(m -> {
-                    JsonObject jo = new JsonObject();
-                    jo.put("name", m.getScheme());
-                    jo.put("title", m.getTitle());
-                    jo.put("description", m.getDescription());
-                    jo.put("label", m.getLabel());
-                    return jo;
-                })
-                .collect(Collectors.toList());
-
-        JsonObject response = new JsonObject();
-        response.put("count", results.size());
-        response.put("components", results);
-        return response.toJson();
-    }
-
-    private String executeCatalogComponentDoc(JsonObject args) {
-        String component = args.getString("component");
-        if (component == null || component.isBlank()) {
-            return "Error: component name is required";
-        }
-        CamelCatalog catalog = getCatalog();
-        ComponentModel model = catalog.componentModel(component);
-        if (model == null) {
-            return "Component not found: " + component;
-        }
-
-        JsonObject response = new JsonObject();
-        response.put("name", model.getScheme());
-        response.put("title", model.getTitle());
-        response.put("description", model.getDescription());
-        response.put("syntax", model.getSyntax());
-        response.put("consumerOnly", model.isConsumerOnly());
-        response.put("producerOnly", model.isProducerOnly());
-
-        List<JsonObject> options = new ArrayList<>();
-        if (model.getEndpointOptions() != null) {
-            model.getEndpointOptions().stream()
-                    .filter(opt -> !opt.isDeprecated())
-                    .forEach(opt -> {
-                        JsonObject jo = new JsonObject();
-                        jo.put("name", opt.getName());
-                        jo.put("description", opt.getDescription());
-                        jo.put("type", opt.getType());
-                        jo.put("required", opt.isRequired());
-                        if (opt.getDefaultValue() != null) {
-                            jo.put("defaultValue", 
opt.getDefaultValue().toString());
-                        }
-                        options.add(jo);
-                    });
-        }
-        response.put("options", options);
-        return response.toJson();
-    }
-
-    private String executeCatalogEips(JsonObject args) {
-        String filter = args.getString("filter");
-        CamelCatalog catalog = getCatalog();
-
-        List<JsonObject> results = catalog.findModelNames().stream()
-                .map(catalog::eipModel)
-                .filter(m -> m != null)
-                .filter(m -> matchesFilter(m.getName(), m.getTitle(), 
m.getDescription(), filter))
-                .limit(20)
-                .map(m -> {
-                    JsonObject jo = new JsonObject();
-                    jo.put("name", m.getName());
-                    jo.put("title", m.getTitle());
-                    jo.put("description", m.getDescription());
-                    jo.put("label", m.getLabel());
-                    return jo;
-                })
-                .collect(Collectors.toList());
-
-        JsonObject response = new JsonObject();
-        response.put("count", results.size());
-        response.put("eips", results);
-        return response.toJson();
-    }
-
-    private static boolean matchesFilter(String name, String title, String 
description, String filter) {
-        if (filter == null || filter.isBlank()) {
-            return true;
-        }
-        String lf = filter.toLowerCase();
-        return (name != null && name.toLowerCase().contains(lf))
-                || (title != null && title.toLowerCase().contains(lf))
-                || (description != null && 
description.toLowerCase().contains(lf));
-    }
-
-    // ---- Example tools ----
-
-    @SuppressWarnings("unchecked")
-    private String executeListExamples(JsonObject args) {
-        String filter = args.getString("filter");
-        String level = args.getString("level");
-
-        List<JsonObject> catalog = ExampleHelper.loadCatalog();
-        List<JsonObject> filtered = ExampleHelper.filterExamples(catalog, 
filter);
-
-        List<JsonObject> results = new ArrayList<>();
-        for (JsonObject entry : filtered) {
-            if (level != null && !level.isBlank()) {
-                String entryLevel = entry.getString("level");
-                if (entryLevel == null || !entryLevel.equalsIgnoreCase(level)) 
{
-                    continue;
-                }
-            }
-            JsonObject jo = new JsonObject();
-            jo.put("name", entry.getString("name"));
-            jo.put("title", entry.getString("title"));
-            jo.put("description", entry.getString("description"));
-            jo.put("level", entry.getString("level"));
-            jo.put("tags", entry.get("tags"));
-            jo.put("bundled", ExampleHelper.isBundled(entry));
-            jo.put("files", ExampleHelper.getFiles(entry));
-            results.add(jo);
-
-            if (results.size() >= 20) {
-                break;
-            }
-        }
-
-        JsonObject response = new JsonObject();
-        response.put("count", results.size());
-        response.put("examples", results);
-        return response.toJson();
-    }
-
-    private String executeGetExampleFile(JsonObject args) {
-        String example = args.getString("example");
-        String file = args.getString("file");
-        if (example == null || example.isBlank()) {
-            return "Error: example name is required";
-        }
-        if (file == null || file.isBlank()) {
-            return "Error: file name is required";
-        }
-
-        List<JsonObject> catalog = ExampleHelper.loadCatalog();
-        JsonObject entry = ExampleHelper.findExample(catalog, example);
-        if (entry == null) {
-            return "Example not found: " + example;
-        }
-
-        List<String> files = ExampleHelper.getFiles(entry);
-        if (!files.contains(file)) {
-            return "File '" + file + "' not found in example '" + example + 
"'. Available files: " + files;
-        }
-
-        if (ExampleHelper.isBundled(entry)) {
-            String resourcePath = "examples/" + example + "/" + file;
-            try (InputStream is = 
ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) {
-                if (is != null) {
-                    return IOHelper.loadText(is);
-                }
-            } catch (Exception e) {
-                return "Error reading file: " + e.getMessage();
-            }
-            return "Could not read bundled file: " + resourcePath;
-        } else {
-            return "This example is not bundled. View it on GitHub: " + 
ExampleHelper.getGithubUrl(entry) + "/" + file;
-        }
-    }
-
-    // ---- CLI tools ----
-
-    @SuppressWarnings("unchecked")
-    private List<JsonObject> loadCommandMetadata() {
-        if (commandMetadataCache != null) {
-            return commandMetadataCache;
-        }
-        try (InputStream is = getClass().getClassLoader()
-                
.getResourceAsStream("META-INF/camel-jbang-commands-metadata.json")) {
-            if (is == null) {
-                return List.of();
-            }
-            String json = IOHelper.loadText(is);
-            JsonObject root = (JsonObject) Jsoner.deserialize(json);
-            Object commands = root.get("commands");
-            if (commands instanceof Collection<?>) {
-                commandMetadataCache = ((Collection<Object>) commands).stream()
-                        .filter(JsonObject.class::isInstance)
-                        .map(JsonObject.class::cast)
-                        .toList();
-                return commandMetadataCache;
-            }
-        } catch (Exception e) {
-            printer().printErr("Failed to load CLI command metadata: " + 
e.getMessage());
-        }
-        return List.of();
-    }
+    // ---- CLI helper methods (used by AskTools) ----
 
     @SuppressWarnings("unchecked")
     static void collectCommands(List<JsonObject> commands, List<JsonObject> 
result, String filter) {
@@ -879,18 +304,6 @@ public class Ask extends CamelCommand {
         }
     }
 
-    private String executeCliListCommands(JsonObject args) {
-        String filter = args.getString("filter");
-        List<JsonObject> commands = loadCommandMetadata();
-        List<JsonObject> result = new ArrayList<>();
-        collectCommands(commands, result, filter);
-
-        JsonObject response = new JsonObject();
-        response.put("count", result.size());
-        response.put("commands", result);
-        return response.toJson();
-    }
-
     @SuppressWarnings("unchecked")
     static JsonObject findCommand(List<JsonObject> commands, String fullName) {
         for (JsonObject cmd : commands) {
@@ -913,128 +326,6 @@ public class Ask extends CamelCommand {
         return null;
     }
 
-    @SuppressWarnings("unchecked")
-    private String executeCliCommandHelp(JsonObject args) {
-        String command = args.getString("command");
-        if (command == null || command.isBlank()) {
-            return "Error: command name is required";
-        }
-
-        List<JsonObject> commands = loadCommandMetadata();
-        JsonObject cmd = findCommand(commands, command);
-        if (cmd == null) {
-            return "Command not found: " + command + ". Use cli_list_commands 
to see available commands.";
-        }
-
-        JsonObject response = new JsonObject();
-        response.put("command", cmd.getString("fullName"));
-        response.put("description", cmd.getString("description"));
-
-        Object options = cmd.get("options");
-        if (options instanceof Collection<?>) {
-            List<JsonObject> opts = ((Collection<Object>) options).stream()
-                    .filter(JsonObject.class::isInstance)
-                    .map(JsonObject.class::cast)
-                    .map(opt -> {
-                        JsonObject o = new JsonObject();
-                        o.put("names", opt.getString("names"));
-                        o.put("description", opt.getString("description"));
-                        o.put("type", opt.getString("type"));
-                        String dv = opt.getString("defaultValue");
-                        if (dv != null) {
-                            o.put("defaultValue", dv);
-                        }
-                        return o;
-                    })
-                    .toList();
-            response.put("options", opts);
-        }
-
-        Object subs = cmd.get("subcommands");
-        if (subs instanceof Collection<?> subList && !subList.isEmpty()) {
-            List<JsonObject> subSummaries = ((Collection<Object>) 
subList).stream()
-                    .filter(JsonObject.class::isInstance)
-                    .map(JsonObject.class::cast)
-                    .map(sub -> {
-                        JsonObject s = new JsonObject();
-                        s.put("command", sub.getString("fullName"));
-                        s.put("description", sub.getString("description"));
-                        return s;
-                    })
-                    .toList();
-            response.put("subcommands", subSummaries);
-        }
-
-        return response.toJson();
-    }
-
-    private String executeCliExec(JsonObject args) {
-        String command = args.getString("command");
-        if (command == null || command.isBlank()) {
-            return "Error: command is required";
-        }
-
-        picocli.CommandLine commandLine = CamelJBangMain.getCommandLine();
-        if (commandLine == null) {
-            return "Error: CLI not available";
-        }
-
-        String[] cmdArgs = tokenizeCommand(command.trim());
-
-        // capture output by temporarily swapping the Printer on main
-        StringBuilder captured = new StringBuilder();
-        Printer capturingPrinter = new Printer() {
-            @Override
-            public void println() {
-                captured.append('\n');
-            }
-
-            @Override
-            public void println(String line) {
-                captured.append(line).append('\n');
-            }
-
-            @Override
-            public void print(String output) {
-                captured.append(output);
-            }
-
-            @Override
-            public void printf(String format, Object... fmtArgs) {
-                captured.append(String.format(format, fmtArgs));
-            }
-        };
-
-        // also capture PicoCLI's own output (usage/help text)
-        StringWriter sw = new StringWriter();
-        PrintWriter pw = new PrintWriter(sw);
-        PrintWriter originalOut = commandLine.getOut();
-        PrintWriter originalErr = commandLine.getErr();
-        commandLine.setOut(pw);
-        commandLine.setErr(pw);
-
-        Printer originalPrinter = getMain().getOut();
-        getMain().setOut(capturingPrinter);
-        try {
-            int exitCode = commandLine.execute(cmdArgs);
-            pw.flush();
-            String output = captured.toString() + sw.toString();
-            if (output.isBlank() && exitCode != 0) {
-                return "Command exited with code " + exitCode;
-            }
-            if (output.length() > 32768) {
-                output = output.substring(0, 32768) + "\n... (truncated)";
-            }
-            return output;
-        } catch (Exception e) {
-            return "Error executing command: " + e.getMessage();
-        } finally {
-            getMain().setOut(originalPrinter);
-            commandLine.setOut(originalOut);
-            commandLine.setErr(originalErr);
-        }
-    }
-
     static String[] tokenizeCommand(String command) {
         List<String> tokens = new ArrayList<>();
         StringBuilder current = new StringBuilder();
@@ -1062,116 +353,7 @@ public class Ask extends CamelCommand {
         return tokens.toArray(String[]::new);
     }
 
-    // ---- File tools ----
-
-    private String executeListFiles(JsonObject args) throws IOException {
-        String pathStr = args.getString("path");
-        Path cwd = Path.of("").toAbsolutePath().normalize();
-        Path base = cwd.resolve(pathStr != null && !pathStr.isBlank() ? 
pathStr : ".").normalize();
-
-        if (!base.startsWith(cwd)) {
-            return "Error: path must be within the current working directory";
-        }
-        if (!Files.isDirectory(base)) {
-            return "Error: not a directory: " + cwd.relativize(base);
-        }
-
-        try (Stream<Path> stream = Files.walk(base, 2)) {
-            List<String> files = stream
-                    .filter(p -> !p.equals(base))
-                    .map(p -> cwd.relativize(p).toString() + 
(Files.isDirectory(p) ? "/" : ""))
-                    .sorted()
-                    .toList();
-
-            JsonObject response = new JsonObject();
-            response.put("directory", base.equals(cwd) ? "." : 
cwd.relativize(base).toString());
-            response.put("count", files.size());
-            response.put("files", files);
-            return response.toJson();
-        }
-    }
-
-    private String executeReadFile(JsonObject args) throws IOException {
-        String fileStr = args.getString("file");
-        if (fileStr == null || fileStr.isBlank()) {
-            return "Error: file path is required";
-        }
-
-        Path cwd = Path.of("").toAbsolutePath().normalize();
-        Path filePath = cwd.resolve(fileStr).normalize();
-
-        if (!filePath.startsWith(cwd)) {
-            return "Error: file path must be within the current working 
directory";
-        }
-        if (!Files.exists(filePath)) {
-            return "File not found: " + cwd.relativize(filePath);
-        }
-
-        String content = Files.readString(filePath);
-        if (content.length() > 32768) {
-            content = content.substring(0, 32768) + "\n... (truncated, file is 
" + content.length() + " bytes)";
-        }
-        return content;
-    }
-
-    private String executeWriteFile(JsonObject args) throws IOException {
-        String fileStr = args.getString("file");
-        String content = args.getString("content");
-        if (fileStr == null || fileStr.isBlank()) {
-            return "Error: file path is required";
-        }
-        if (content == null) {
-            return "Error: content is required";
-        }
-
-        Path cwd = Path.of("").toAbsolutePath().normalize();
-        Path filePath = cwd.resolve(fileStr).normalize();
-
-        if (!filePath.startsWith(cwd)) {
-            return "Error: file path must be within the current working 
directory";
-        }
-
-        Files.createDirectories(filePath.getParent());
-        Files.writeString(filePath, content);
-        return "File written: " + cwd.relativize(filePath);
-    }
-
-    private CamelCatalog getCatalog() {
-        if (catalog == null) {
-            catalog = new DefaultCamelCatalog();
-        }
-        return catalog;
-    }
-
-    // ---- JSON schema helpers for tool parameters ----
-
-    private static JsonObject emptyParams() {
-        JsonObject schema = new JsonObject();
-        schema.put("type", "object");
-        schema.put("properties", new JsonObject());
-        return schema;
-    }
-
-    private static JsonObject objectParams(Map<String, JsonObject> properties) 
{
-        JsonObject props = new JsonObject();
-        Map<String, JsonObject> ordered = new LinkedHashMap<>(properties);
-        for (Map.Entry<String, JsonObject> entry : ordered.entrySet()) {
-            props.put(entry.getKey(), entry.getValue());
-        }
-        JsonObject schema = new JsonObject();
-        schema.put("type", "object");
-        schema.put("properties", props);
-        return schema;
-    }
-
-    private static JsonObject stringProp(String description) {
-        JsonObject prop = new JsonObject();
-        prop.put("type", "string");
-        prop.put("description", description);
-        return prop;
-    }
-
-    private static String truncate(String text, int maxLen) {
+    static String truncate(String text, int maxLen) {
         if (text == null) {
             return "null";
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
similarity index 70%
copy from 
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
copy to 
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
index 9a7e5af16761..50c196a83a92 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
@@ -32,7 +32,6 @@ import java.util.stream.Stream;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
-import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
 import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
@@ -40,292 +39,37 @@ import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
-import org.jline.reader.EndOfFileException;
-import org.jline.reader.LineReader;
-import org.jline.reader.LineReaderBuilder;
-import org.jline.reader.UserInterruptException;
-import org.jline.terminal.Terminal;
-import org.jline.terminal.TerminalBuilder;
-import picocli.CommandLine.Command;
-import picocli.CommandLine.Option;
-import picocli.CommandLine.Parameters;
 
 /**
- * Ask a question about a running Camel application using AI with tool 
calling. The LLM can inspect the live runtime
- * (routes, health, traces, etc.) to provide informed answers.
+ * Shared tool definitions and execution logic for the Camel AI assistant. 
Used by both the {@code camel ask} CLI
+ * command and the TUI AI panel.
  */
-@Command(name = "ask",
-         description = "Ask a question about a running Camel application using 
AI",
-         sortOptions = false, showDefaultValues = true,
-         footer = {
-                 "%nExamples:",
-                 "  camel ask \"what routes are running?\"",
-                 "  camel ask \"why is my route failing?\" --name=myApp",
-                 "  camel ask \"show me the route structure\" 
--api-type=anthropic",
-                 "  camel ask \"are there any blocked exchanges?\" 
--model=gpt-4",
-                 "  camel ask                                   (interactive 
chat)" })
-public class Ask extends CamelCommand {
-
-    private static final String DEFAULT_MODEL = "llama3.2";
+public class AskTools {
+
     private static final String NO_PROCESS
             = "No running Camel process connected. Start one with: camel run 
<file>";
 
-    @Parameters(description = "Question to ask (omit for interactive chat 
mode)", arity = "0..*")
-    List<String> question;
-
-    @Option(names = { "--url" },
-            description = "LLM API endpoint URL. Auto-detected if not 
specified.")
-    String url;
-
-    @Option(names = { "--api-type" },
-            description = "API type: 'ollama', 'openai', or 'anthropic'")
-    LlmClient.ApiType apiType;
-
-    @Option(names = { "--api-key" },
-            description = "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars")
-    String apiKey;
-
-    @Option(names = { "--model" },
-            description = "Model to use",
-            defaultValue = DEFAULT_MODEL)
-    String model = DEFAULT_MODEL;
-
-    @Option(names = { "--timeout" },
-            description = "Timeout in seconds for LLM response",
-            defaultValue = "120")
-    int timeout = 120;
-
-    @Option(names = { "--name" },
-            description = "Name or PID of the Camel process. Auto-detected 
when exactly one process is running")
-    String nameOrPid;
-
-    @Option(names = { "--max-iterations" },
-            description = "Maximum number of tool-calling rounds",
-            defaultValue = "10")
-    int maxIterations = 10;
-
-    @Option(names = { "--show-tools" },
-            description = "Show tool calls and results as they happen")
-    boolean showTools;
-
-    @Option(names = { "--verbose" },
-            description = "Print debug information: HTTP requests, responses, 
and parsed results")
-    boolean verbose;
-
     private long targetPid;
     private CamelCatalog catalog;
     private volatile List<JsonObject> commandMetadataCache;
 
-    public Ask(CamelJBangMain main) {
-        super(main);
+    public AskTools(long targetPid) {
+        this.targetPid = targetPid;
     }
 
-    @Override
-    public Integer doCall() throws Exception {
-        LlmClient client = LlmClient.create()
-                .withUrl(url)
-                .withApiType(apiType)
-                .withApiKey(apiKey)
-                .withModel(model)
-                .withTimeout(timeout)
-                .withTemperature(0.3)
-                .withStream(true)
-                .withMaxTokens(4096)
-                .withVerbose(verbose)
-                .withPrinter(printer());
-
-        if (!client.detectEndpoint()) {
-            printer().printErr("LLM service is not reachable.");
-            printer().printErr("Options: --url=<endpoint>, 
--api-type=anthropic, or start Ollama with: camel infra run ollama");
-            return 1;
-        }
-
-        RuntimeHelper.ProcessInfo process = findProcess(nameOrPid);
-        if (process != null) {
-            targetPid = process.pid();
-        } else if (nameOrPid != null && !nameOrPid.isBlank()) {
-            return 1;
-        } else {
-            targetPid = -1;
-        }
-
-        String systemPrompt = buildSystemPrompt(process);
-        List<LlmClient.ToolDef> tools = buildToolDefinitions();
-
-        if (question == null || question.isEmpty()) {
-            return runInteractiveChat(client, process, systemPrompt, tools);
-        }
-
-        String userQuestion = String.join(" ", question);
-        printer().println("Using " + client.model + " (" + client.apiType + ") 
to answer your question...");
-        if (process != null) {
-            printer().println("Target: " + process.name() + " (PID " + 
process.pid() + ")");
-        }
-        printer().println();
-
-        List<LlmClient.Message> messages = new ArrayList<>();
-        return runAgentLoop(client, systemPrompt, tools, messages, 
userQuestion);
+    public long getTargetPid() {
+        return targetPid;
     }
 
-    private int runInteractiveChat(
-            LlmClient client, RuntimeHelper.ProcessInfo process,
-            String systemPrompt, List<LlmClient.ToolDef> tools)
-            throws Exception {
-        Terminal terminal = EnvironmentHelper.getActiveTerminal();
-        if (terminal == null) {
-            terminal = TerminalBuilder.builder().system(true).build();
-        }
-        LineReader reader = 
LineReaderBuilder.builder().terminal(terminal).build();
-
-        printer().println("Camel AI Assistant (" + client.model + ", " + 
client.apiType + ")");
-        if (process != null) {
-            printer().println("Target: " + process.name() + " (PID " + 
process.pid() + ")");
-        }
-        printer().println("Type your question, or 'exit' to quit.");
-        printer().println();
-
-        List<LlmClient.Message> messages = new ArrayList<>();
-
-        while (true) {
-            String line;
-            try {
-                line = reader.readLine("ask> ");
-            } catch (UserInterruptException | EndOfFileException e) {
-                break;
-            }
-            if (line == null || line.isBlank() || 
"exit".equalsIgnoreCase(line.strip())) {
-                break;
-            }
-
-            int result = runAgentLoop(client, systemPrompt, tools, messages, 
line.strip());
-            if (result != 0) {
-                printer().printErr("(error processing question, 
continuing...)");
-            }
-            printer().println();
-        }
-        return 0;
-    }
-
-    private int runAgentLoop(
-            LlmClient client, String systemPrompt,
-            List<LlmClient.ToolDef> tools, List<LlmClient.Message> messages,
-            String userQuestion) {
-        messages.add(LlmClient.Message.user(userQuestion));
-
-        for (int i = 0; i < maxIterations; i++) {
-            LlmClient.ChatResponse response = 
client.chatWithTools(systemPrompt, messages, tools);
-            if (response == null) {
-                printer().printErr("Failed to get response from LLM");
-                return 1;
-            }
-
-            if (response.toolCalls() != null && 
!response.toolCalls().isEmpty()) {
-                
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
response.toolCalls()));
-
-                List<LlmClient.ToolResult> results = new ArrayList<>();
-                for (LlmClient.ToolCall toolCall : response.toolCalls()) {
-                    if (showTools) {
-                        printer().println("[tool] " + toolCall.name() + "(" + 
toolCall.arguments().toJson() + ")");
-                    }
-                    String result = executeTool(toolCall.name(), 
toolCall.arguments());
-                    if (showTools) {
-                        printer().println("[result] " + truncate(result, 200));
-                    }
-                    results.add(new LlmClient.ToolResult(toolCall.id(), 
result));
-                }
-                messages.add(LlmClient.Message.toolResults(results));
-            } else {
-                if (!response.streamed() && response.text() != null) {
-                    printer().println(response.text());
-                }
-                
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
List.of()));
-                return 0;
-            }
-        }
-
-        printer().printErr("Reached maximum iterations (" + maxIterations + ") 
without a final answer.");
-        return 1;
-    }
-
-    // ---- Process discovery (delegates to RuntimeHelper) ----
-
-    private RuntimeHelper.ProcessInfo findProcess(String nameOrPid) {
-        // Fall back to TUI-selected process if no explicit name given
-        if ((nameOrPid == null || nameOrPid.isBlank()) && 
EnvironmentHelper.getSelectedProcess() != null) {
-            nameOrPid = EnvironmentHelper.getSelectedProcess();
-        }
-
-        RuntimeHelper.ProcessInfo found = RuntimeHelper.findProcess(nameOrPid);
-        if (found != null) {
-            return found;
-        }
-
-        List<RuntimeHelper.ProcessInfo> processes = 
RuntimeHelper.discoverProcesses();
-        if (nameOrPid != null && !nameOrPid.isBlank()) {
-            if (processes.isEmpty()) {
-                printer().printErr("No running Camel processes found.");
-                printer().printErr("Start a Camel application first: camel run 
myRoute.yaml");
-            } else if (processes.size() > 1) {
-                printer().printErr("No unique Camel process found matching: " 
+ nameOrPid);
-                processes.forEach(p -> printer().printErr("  " + p.name() + " 
(PID " + p.pid() + ")"));
-                printer().printErr("Specify a more specific name or PID with 
--name");
-            } else {
-                printer().printErr("No Camel process found matching: " + 
nameOrPid);
-            }
-        } else if (processes.size() > 1) {
-            printer().println("Multiple Camel processes found. Use --name to 
specify one:");
-            processes.forEach(p -> printer().println("  " + p.name() + " (PID 
" + p.pid() + ")"));
-            printer().println();
-        }
-        return null;
-    }
-
-    // ---- System prompt ----
-
-    private String buildSystemPrompt(RuntimeHelper.ProcessInfo process) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("You are an Apache Camel assistant. ");
-        sb.append("You help users build, understand, and troubleshoot Camel 
applications.\n\n");
-
-        if (process != null) {
-            sb.append("You are connected to a running Camel application: ");
-            sb.append(process.name()).append(" (PID 
").append(process.pid()).append("). ");
-            sb.append("Use the runtime inspection tools to gather information 
about it.\n\n");
-        } else {
-            List<RuntimeHelper.ProcessInfo> available = 
RuntimeHelper.discoverProcesses();
-            if (!available.isEmpty()) {
-                sb.append("No Camel process is currently selected. ");
-                sb.append("Use list_processes to see available processes, then 
select_process to connect to one. ");
-                sb.append("Runtime inspection tools will not work until a 
process is selected.\n\n");
-            }
-        }
-
-        sb.append("You can search the Camel catalog (components, EIPs), browse 
examples, ");
-        sb.append("read/write files, and execute any Camel CLI command.\n\n");
-        sb.append("For CLI commands beyond the built-in tools, use 
cli_list_commands to discover ");
-        sb.append("available commands, cli_command_help to see options, and 
cli_exec to run them.\n\n");
-        sb.append("Guidelines:\n");
-        sb.append("- When creating routes, use YAML DSL format (Camel's 
recommended format for JBang)\n");
-        sb.append("- Look at existing files first with list_files/read_file 
before creating new ones\n");
-        sb.append("- Use catalog tools to look up component syntax before 
writing routes\n");
-        sb.append("- Use examples as reference when building new routes\n");
-        sb.append("- Be concise and actionable in your answers\n");
-        sb.append("- Format output as plain text for terminal display, do not 
use markdown\n");
-        if (process != null) {
-            sb.append("- Start by gathering relevant information using the 
available runtime tools\n");
-            sb.append("- If something looks wrong, explain what it means and 
suggest fixes\n");
-            sb.append("- To stop routes or the application, always use the 
provided tools ");
-            sb.append("(stop_route, stop_application) for graceful shutdown. 
Never suggest kill or kill -9.\n");
-        }
-        return sb.toString();
+    public void setTargetPid(long targetPid) {
+        this.targetPid = targetPid;
     }
 
     // ---- Tool definitions ----
 
-    private List<LlmClient.ToolDef> buildToolDefinitions() {
+    public List<LlmClient.ToolDef> buildToolDefinitions() {
         List<LlmClient.ToolDef> tools = new ArrayList<>();
 
-        // Process discovery and selection
         tools.add(new LlmClient.ToolDef(
                 "list_processes",
                 "List all running Camel processes with their PID and name. Use 
this to discover available processes before selecting one.",
@@ -336,7 +80,6 @@ public class Ask extends CamelCommand {
                 objectParams(Map.of(
                         "name", stringProp("Name or PID of the Camel process 
to connect to")))));
 
-        // Status-file tools (no parameters needed)
         tools.add(new LlmClient.ToolDef(
                 "get_context",
                 "Get Camel context info: name, version, state, uptime, route 
count, exchange statistics.",
@@ -370,7 +113,6 @@ public class Ask extends CamelCommand {
                 "Show configuration properties of the running Camel 
application.",
                 emptyParams()));
 
-        // IPC action tools (with parameters)
         tools.add(new LlmClient.ToolDef(
                 "get_route_source",
                 "Get the source code of routes. Use filter to limit by 
filename (supports wildcards).",
@@ -397,7 +139,6 @@ public class Ask extends CamelCommand {
                 objectParams(Map.of(
                         "action", stringProp("Action: enable, disable, or 
dump")))));
 
-        // Route lifecycle tools
         tools.add(new LlmClient.ToolDef(
                 "stop_route",
                 "Gracefully stop a route. The route will finish processing 
in-flight exchanges before stopping.",
@@ -419,13 +160,11 @@ public class Ask extends CamelCommand {
                 objectParams(Map.of(
                         "routeId", stringProp("The ID of the route to 
resume")))));
 
-        // Application lifecycle
         tools.add(new LlmClient.ToolDef(
                 "stop_application",
                 "Gracefully stop the Camel application. The application will 
finish processing in-flight exchanges and shut down cleanly. Use this instead 
of kill.",
                 emptyParams()));
 
-        // Catalog tools
         tools.add(new LlmClient.ToolDef(
                 "catalog_components",
                 "Search the Camel component catalog by name or label. Returns 
component name, title, description, and labels.",
@@ -443,7 +182,6 @@ public class Ask extends CamelCommand {
                 objectParams(Map.of(
                         "filter", stringProp("Filter by name, title, or 
description (case-insensitive substring)")))));
 
-        // Example tools
         tools.add(new LlmClient.ToolDef(
                 "list_examples",
                 "List available Camel CLI examples. Returns name, title, 
description, difficulty level, and tags.",
@@ -457,7 +195,6 @@ public class Ask extends CamelCommand {
                         "example", stringProp("Example name (e.g., timer-log, 
rest-api, circuit-breaker)"),
                         "file", stringProp("File name within the example 
(e.g., route.camel.yaml)")))));
 
-        // CLI tools (access to all camel CLI commands)
         tools.add(new LlmClient.ToolDef(
                 "cli_list_commands",
                 "List available Camel CLI commands. Returns command names and 
descriptions. Use filter to narrow results.",
@@ -475,7 +212,6 @@ public class Ask extends CamelCommand {
                         "command", stringProp(
                                 "The full command line to execute (e.g., 'get 
error --diagram', 'catalog component --filter=kafka')")))));
 
-        // File tools
         tools.add(new LlmClient.ToolDef(
                 "list_files",
                 "List files in a directory (up to 2 levels deep). Defaults to 
current working directory.",
@@ -498,10 +234,9 @@ public class Ask extends CamelCommand {
 
     // ---- Tool execution ----
 
-    private String executeTool(String name, JsonObject args) {
+    public String executeTool(String name, JsonObject args) {
         try {
             return switch (name) {
-                // Runtime tools (require a running process)
                 case "list_processes" -> executeListProcesses();
                 case "select_process" -> executeSelectProcess(args);
                 case "get_context" ->
@@ -531,18 +266,14 @@ public class Ask extends CamelCommand {
                 case "suspend_route" -> targetPid < 0 ? NO_PROCESS : 
executeRouteCommand(args, "suspend");
                 case "resume_route" -> targetPid < 0 ? NO_PROCESS : 
executeRouteCommand(args, "resume");
                 case "stop_application" -> targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.stopApplication(targetPid);
-                // Catalog tools
                 case "catalog_components" -> executeCatalogComponents(args);
                 case "catalog_component_doc" -> 
executeCatalogComponentDoc(args);
                 case "catalog_eips" -> executeCatalogEips(args);
-                // Example tools
                 case "list_examples" -> executeListExamples(args);
                 case "get_example_file" -> executeGetExampleFile(args);
-                // CLI tools
                 case "cli_list_commands" -> executeCliListCommands(args);
                 case "cli_command_help" -> executeCliCommandHelp(args);
                 case "cli_exec" -> executeCliExec(args);
-                // File tools
                 case "list_files" -> executeListFiles(args);
                 case "read_file" -> executeReadFile(args);
                 case "write_file" -> executeWriteFile(args);
@@ -553,6 +284,48 @@ public class Ask extends CamelCommand {
         }
     }
 
+    // ---- System prompt ----
+
+    public static String buildSystemPrompt(long targetPid, String processName) 
{
+        StringBuilder sb = new StringBuilder();
+        sb.append("You are an Apache Camel assistant. ");
+        sb.append("You help users build, understand, and troubleshoot Camel 
applications.\n\n");
+
+        if (targetPid >= 0 && processName != null) {
+            sb.append("You are connected to a running Camel application: ");
+            sb.append(processName).append(" (PID 
").append(targetPid).append("). ");
+            sb.append("Use the runtime inspection tools to gather information 
about it.\n\n");
+        } else {
+            List<RuntimeHelper.ProcessInfo> available = 
RuntimeHelper.discoverProcesses();
+            if (!available.isEmpty()) {
+                sb.append("No Camel process is currently selected. ");
+                sb.append("Use list_processes to see available processes, then 
select_process to connect to one. ");
+                sb.append("Runtime inspection tools will not work until a 
process is selected.\n\n");
+            }
+        }
+
+        sb.append("You can search the Camel catalog (components, EIPs), browse 
examples, ");
+        sb.append("read/write files, and execute any Camel CLI command.\n\n");
+        sb.append("For CLI commands beyond the built-in tools, use 
cli_list_commands to discover ");
+        sb.append("available commands, cli_command_help to see options, and 
cli_exec to run them.\n\n");
+        sb.append("Guidelines:\n");
+        sb.append("- When creating routes, use YAML DSL format (Camel's 
recommended format for JBang)\n");
+        sb.append("- Look at existing files first with list_files/read_file 
before creating new ones\n");
+        sb.append("- Use catalog tools to look up component syntax before 
writing routes\n");
+        sb.append("- Use examples as reference when building new routes\n");
+        sb.append("- Be concise and actionable in your answers\n");
+        sb.append("- Format output as plain text for terminal display, do not 
use markdown\n");
+        if (targetPid >= 0) {
+            sb.append("- Start by gathering relevant information using the 
available runtime tools\n");
+            sb.append("- If something looks wrong, explain what it means and 
suggest fixes\n");
+            sb.append("- To stop routes or the application, always use the 
provided tools ");
+            sb.append("(stop_route, stop_application) for graceful shutdown. 
Never suggest kill or kill -9.\n");
+        }
+        return sb.toString();
+    }
+
+    // ---- Runtime tool execution ----
+
     private String executeListProcesses() {
         List<RuntimeHelper.ProcessInfo> processes = 
RuntimeHelper.discoverProcesses();
         if (processes.isEmpty()) {
@@ -646,10 +419,10 @@ public class Ask extends CamelCommand {
     private String executeCatalogComponents(JsonObject args) {
         String filter = args.getString("filter");
         String label = args.getString("label");
-        CamelCatalog catalog = getCatalog();
+        CamelCatalog cat = getCatalog();
 
-        List<JsonObject> results = catalog.findComponentNames().stream()
-                .map(catalog::componentModel)
+        List<JsonObject> results = cat.findComponentNames().stream()
+                .map(cat::componentModel)
                 .filter(m -> m != null)
                 .filter(m -> matchesFilter(m.getScheme(), m.getTitle(), 
m.getDescription(), filter))
                 .filter(m -> label == null || label.isBlank()
@@ -676,8 +449,8 @@ public class Ask extends CamelCommand {
         if (component == null || component.isBlank()) {
             return "Error: component name is required";
         }
-        CamelCatalog catalog = getCatalog();
-        ComponentModel model = catalog.componentModel(component);
+        CamelCatalog cat = getCatalog();
+        ComponentModel model = cat.componentModel(component);
         if (model == null) {
             return "Component not found: " + component;
         }
@@ -712,10 +485,10 @@ public class Ask extends CamelCommand {
 
     private String executeCatalogEips(JsonObject args) {
         String filter = args.getString("filter");
-        CamelCatalog catalog = getCatalog();
+        CamelCatalog cat = getCatalog();
 
-        List<JsonObject> results = catalog.findModelNames().stream()
-                .map(catalog::eipModel)
+        List<JsonObject> results = cat.findModelNames().stream()
+                .map(cat::eipModel)
                 .filter(m -> m != null)
                 .filter(m -> matchesFilter(m.getName(), m.getTitle(), 
m.getDescription(), filter))
                 .limit(20)
@@ -735,16 +508,6 @@ public class Ask extends CamelCommand {
         return response.toJson();
     }
 
-    private static boolean matchesFilter(String name, String title, String 
description, String filter) {
-        if (filter == null || filter.isBlank()) {
-            return true;
-        }
-        String lf = filter.toLowerCase();
-        return (name != null && name.toLowerCase().contains(lf))
-                || (title != null && title.toLowerCase().contains(lf))
-                || (description != null && 
description.toLowerCase().contains(lf));
-    }
-
     // ---- Example tools ----
 
     @SuppressWarnings("unchecked")
@@ -752,8 +515,8 @@ public class Ask extends CamelCommand {
         String filter = args.getString("filter");
         String level = args.getString("level");
 
-        List<JsonObject> catalog = ExampleHelper.loadCatalog();
-        List<JsonObject> filtered = ExampleHelper.filterExamples(catalog, 
filter);
+        List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
+        List<JsonObject> filtered = ExampleHelper.filterExamples(catalog2, 
filter);
 
         List<JsonObject> results = new ArrayList<>();
         for (JsonObject entry : filtered) {
@@ -794,8 +557,8 @@ public class Ask extends CamelCommand {
             return "Error: file name is required";
         }
 
-        List<JsonObject> catalog = ExampleHelper.loadCatalog();
-        JsonObject entry = ExampleHelper.findExample(catalog, example);
+        List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
+        JsonObject entry = ExampleHelper.findExample(catalog2, example);
         if (entry == null) {
             return "Example not found: " + example;
         }
@@ -843,47 +606,16 @@ public class Ask extends CamelCommand {
                 return commandMetadataCache;
             }
         } catch (Exception e) {
-            printer().printErr("Failed to load CLI command metadata: " + 
e.getMessage());
+            // ignore
         }
         return List.of();
     }
 
-    @SuppressWarnings("unchecked")
-    static void collectCommands(List<JsonObject> commands, List<JsonObject> 
result, String filter) {
-        for (JsonObject cmd : commands) {
-            String fullName = cmd.getString("fullName");
-            String description = cmd.getString("description");
-            boolean matches = filter == null || filter.isBlank()
-                    || (fullName != null && 
fullName.toLowerCase().contains(filter.toLowerCase()))
-                    || (description != null && 
description.toLowerCase().contains(filter.toLowerCase()));
-            if (matches) {
-                JsonObject entry = new JsonObject();
-                entry.put("command", fullName);
-                entry.put("description", description);
-                Object subs = cmd.get("subcommands");
-                if (subs instanceof Collection<?> subList && 
!subList.isEmpty()) {
-                    entry.put("hasSubcommands", true);
-                    entry.put("subcommandCount", subList.size());
-                }
-                result.add(entry);
-            }
-            Object subs = cmd.get("subcommands");
-            if (subs instanceof Collection<?>) {
-                collectCommands(
-                        ((Collection<Object>) subs).stream()
-                                .filter(JsonObject.class::isInstance)
-                                .map(JsonObject.class::cast)
-                                .toList(),
-                        result, filter);
-            }
-        }
-    }
-
     private String executeCliListCommands(JsonObject args) {
         String filter = args.getString("filter");
         List<JsonObject> commands = loadCommandMetadata();
         List<JsonObject> result = new ArrayList<>();
-        collectCommands(commands, result, filter);
+        Ask.collectCommands(commands, result, filter);
 
         JsonObject response = new JsonObject();
         response.put("count", result.size());
@@ -891,28 +623,6 @@ public class Ask extends CamelCommand {
         return response.toJson();
     }
 
-    @SuppressWarnings("unchecked")
-    static JsonObject findCommand(List<JsonObject> commands, String fullName) {
-        for (JsonObject cmd : commands) {
-            if (fullName.equals(cmd.getString("fullName"))) {
-                return cmd;
-            }
-            Object subs = cmd.get("subcommands");
-            if (subs instanceof Collection<?>) {
-                JsonObject found = findCommand(
-                        ((Collection<Object>) subs).stream()
-                                .filter(JsonObject.class::isInstance)
-                                .map(JsonObject.class::cast)
-                                .toList(),
-                        fullName);
-                if (found != null) {
-                    return found;
-                }
-            }
-        }
-        return null;
-    }
-
     @SuppressWarnings("unchecked")
     private String executeCliCommandHelp(JsonObject args) {
         String command = args.getString("command");
@@ -921,7 +631,7 @@ public class Ask extends CamelCommand {
         }
 
         List<JsonObject> commands = loadCommandMetadata();
-        JsonObject cmd = findCommand(commands, command);
+        JsonObject cmd = Ask.findCommand(commands, command);
         if (cmd == null) {
             return "Command not found: " + command + ". Use cli_list_commands 
to see available commands.";
         }
@@ -979,9 +689,8 @@ public class Ask extends CamelCommand {
             return "Error: CLI not available";
         }
 
-        String[] cmdArgs = tokenizeCommand(command.trim());
+        String[] cmdArgs = Ask.tokenizeCommand(command.trim());
 
-        // capture output by temporarily swapping the Printer on main
         StringBuilder captured = new StringBuilder();
         Printer capturingPrinter = new Printer() {
             @Override
@@ -1005,7 +714,7 @@ public class Ask extends CamelCommand {
             }
         };
 
-        // also capture PicoCLI's own output (usage/help text)
+        CamelJBangMain main = (CamelJBangMain) commandLine.getCommand();
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw);
         PrintWriter originalOut = commandLine.getOut();
@@ -1013,8 +722,8 @@ public class Ask extends CamelCommand {
         commandLine.setOut(pw);
         commandLine.setErr(pw);
 
-        Printer originalPrinter = getMain().getOut();
-        getMain().setOut(capturingPrinter);
+        Printer originalPrinter = main.getOut();
+        main.setOut(capturingPrinter);
         try {
             int exitCode = commandLine.execute(cmdArgs);
             pw.flush();
@@ -1029,39 +738,12 @@ public class Ask extends CamelCommand {
         } catch (Exception e) {
             return "Error executing command: " + e.getMessage();
         } finally {
-            getMain().setOut(originalPrinter);
+            main.setOut(originalPrinter);
             commandLine.setOut(originalOut);
             commandLine.setErr(originalErr);
         }
     }
 
-    static String[] tokenizeCommand(String command) {
-        List<String> tokens = new ArrayList<>();
-        StringBuilder current = new StringBuilder();
-        boolean inSingleQuote = false;
-        boolean inDoubleQuote = false;
-
-        for (int i = 0; i < command.length(); i++) {
-            char c = command.charAt(i);
-            if (c == '\'' && !inDoubleQuote) {
-                inSingleQuote = !inSingleQuote;
-            } else if (c == '"' && !inSingleQuote) {
-                inDoubleQuote = !inDoubleQuote;
-            } else if (Character.isWhitespace(c) && !inSingleQuote && 
!inDoubleQuote) {
-                if (!current.isEmpty()) {
-                    tokens.add(current.toString());
-                    current.setLength(0);
-                }
-            } else {
-                current.append(c);
-            }
-        }
-        if (!current.isEmpty()) {
-            tokens.add(current.toString());
-        }
-        return tokens.toArray(String[]::new);
-    }
-
     // ---- File tools ----
 
     private String executeListFiles(JsonObject args) throws IOException {
@@ -1143,16 +825,16 @@ public class Ask extends CamelCommand {
         return catalog;
     }
 
-    // ---- JSON schema helpers for tool parameters ----
+    // ---- JSON schema helpers ----
 
-    private static JsonObject emptyParams() {
+    public static JsonObject emptyParams() {
         JsonObject schema = new JsonObject();
         schema.put("type", "object");
         schema.put("properties", new JsonObject());
         return schema;
     }
 
-    private static JsonObject objectParams(Map<String, JsonObject> properties) 
{
+    public static JsonObject objectParams(Map<String, JsonObject> properties) {
         JsonObject props = new JsonObject();
         Map<String, JsonObject> ordered = new LinkedHashMap<>(properties);
         for (Map.Entry<String, JsonObject> entry : ordered.entrySet()) {
@@ -1164,17 +846,20 @@ public class Ask extends CamelCommand {
         return schema;
     }
 
-    private static JsonObject stringProp(String description) {
+    public static JsonObject stringProp(String description) {
         JsonObject prop = new JsonObject();
         prop.put("type", "string");
         prop.put("description", description);
         return prop;
     }
 
-    private static String truncate(String text, int maxLen) {
-        if (text == null) {
-            return "null";
+    static boolean matchesFilter(String name, String title, String 
description, String filter) {
+        if (filter == null || filter.isBlank()) {
+            return true;
         }
-        return text.length() <= maxLen ? text : text.substring(0, maxLen) + 
"...";
+        String lf = filter.toLowerCase();
+        return (name != null && name.toLowerCase().contains(lf))
+                || (title != null && title.toLowerCase().contains(lf))
+                || (description != null && 
description.toLowerCase().contains(lf));
     }
 }
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 ac87f338677c..d2abee4f8f44 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
@@ -42,7 +42,7 @@ import org.apache.camel.util.json.Jsoner;
 /**
  * Shared LLM HTTP client supporting Ollama, OpenAI-compatible, and Anthropic 
(including Vertex AI) APIs.
  */
-class LlmClient {
+public class LlmClient {
 
     private static final String DEFAULT_OLLAMA_URL = "http://localhost:11434";;
     private static final String DEFAULT_ANTHROPIC_URL = 
"https://api.anthropic.com";;
@@ -58,7 +58,7 @@ class LlmClient {
             "claude-opus-4-5", "claude-opus-4-5@20251101",
             "claude-haiku-4-5", "claude-haiku-4-5@20251001");
 
-    enum ApiType {
+    public enum ApiType {
         ollama,
         openai,
         anthropic
@@ -66,31 +66,31 @@ class LlmClient {
 
     // -- Unified abstractions for tool-calling across API formats --
 
-    record ToolDef(String name, String description, JsonObject parameters) {
+    public record ToolDef(String name, String description, JsonObject 
parameters) {
     }
 
-    record ToolCall(String id, String name, JsonObject arguments) {
+    public record ToolCall(String id, String name, JsonObject arguments) {
     }
 
-    record ToolResult(String toolCallId, String content) {
+    public record ToolResult(String toolCallId, String content) {
     }
 
-    record Message(String role, String content, List<ToolCall> toolCalls, 
List<ToolResult> toolResults) {
+    public record Message(String role, String content, List<ToolCall> 
toolCalls, List<ToolResult> toolResults) {
 
-        static Message user(String text) {
+        public static Message user(String text) {
             return new Message("user", text, null, null);
         }
 
-        static Message assistantWithToolCalls(String text, List<ToolCall> 
calls) {
+        public static Message assistantWithToolCalls(String text, 
List<ToolCall> calls) {
             return new Message("assistant", text, calls, null);
         }
 
-        static Message toolResults(List<ToolResult> results) {
+        public static Message toolResults(List<ToolResult> results) {
             return new Message("tool", null, null, results);
         }
     }
 
-    record ChatResponse(String text, List<ToolCall> toolCalls, String 
stopReason, boolean streamed) {
+    public record ChatResponse(String text, List<ToolCall> toolCalls, String 
stopReason, boolean streamed) {
     }
 
     // -- Configuration --
@@ -104,7 +104,23 @@ class LlmClient {
     boolean stream;
     int maxTokens;
     boolean verbose;
-    Printer printer;
+    Printer printer = new Printer() {
+        @Override
+        public void println() {
+        }
+
+        @Override
+        public void println(String line) {
+        }
+
+        @Override
+        public void print(String output) {
+        }
+
+        @Override
+        public void printf(String format, Object... args) {
+        }
+    };
 
     private final HttpClient httpClient = HttpClient.newBuilder()
             .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS))
@@ -116,63 +132,63 @@ class LlmClient {
 
     // -- Builder --
 
-    static LlmClient create() {
+    public static LlmClient create() {
         return new LlmClient();
     }
 
-    LlmClient withApiType(ApiType apiType) {
+    public LlmClient withApiType(ApiType apiType) {
         this.apiType = apiType;
         return this;
     }
 
-    LlmClient withUrl(String url) {
+    public LlmClient withUrl(String url) {
         this.url = url;
         return this;
     }
 
-    LlmClient withApiKey(String apiKey) {
+    public LlmClient withApiKey(String apiKey) {
         this.apiKey = apiKey;
         return this;
     }
 
-    LlmClient withModel(String model) {
+    public LlmClient withModel(String model) {
         this.model = model;
         return this;
     }
 
-    LlmClient withTimeout(int timeout) {
+    public LlmClient withTimeout(int timeout) {
         this.timeout = timeout;
         return this;
     }
 
-    LlmClient withTemperature(double temperature) {
+    public LlmClient withTemperature(double temperature) {
         this.temperature = temperature;
         return this;
     }
 
-    LlmClient withStream(boolean stream) {
+    public LlmClient withStream(boolean stream) {
         this.stream = stream;
         return this;
     }
 
-    LlmClient withMaxTokens(int maxTokens) {
+    public LlmClient withMaxTokens(int maxTokens) {
         this.maxTokens = maxTokens;
         return this;
     }
 
-    LlmClient withVerbose(boolean verbose) {
+    public LlmClient withVerbose(boolean verbose) {
         this.verbose = verbose;
         return this;
     }
 
-    LlmClient withPrinter(Printer printer) {
+    public LlmClient withPrinter(Printer printer) {
         this.printer = printer;
         return this;
     }
 
     // -- Auto-detection --
 
-    boolean detectEndpoint() {
+    public boolean detectEndpoint() {
         boolean found;
         if (tryExplicitUrl()) {
             found = true;
@@ -232,7 +248,7 @@ class LlmClient {
 
     // -- Chat with tools (for ask) --
 
-    ChatResponse chatWithTools(String systemPrompt, List<Message> messages, 
List<ToolDef> tools) {
+    public ChatResponse chatWithTools(String systemPrompt, List<Message> 
messages, List<ToolDef> tools) {
         return switch (apiType) {
             case ollama -> chatOllamaFormat(systemPrompt, messages, tools);
             case openai -> chatOpenAiFormat(systemPrompt, messages, tools);
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
new file mode 100644
index 000000000000..52d2257bd676
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -0,0 +1,406 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.markdown.MarkdownView;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.commands.AskTools;
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+
+/**
+ * AI prompt panel for the TUI. Communicates directly with an LLM via {@link 
LlmClient} and uses the same tool
+ * definitions as {@code camel ask}. Toggled with F8 when the TUI runs with 
{@code --mcp} mode.
+ */
+class AiPanel {
+
+    private static final int[] SPLIT_PERCENTS = { 25, 50, 75, 100 };
+    private static final int MAX_ITERATIONS = 10;
+
+    private boolean visible;
+    private int splitIndex = 1; // default 50%
+    private MonitorContext ctx;
+
+    // Input state
+    private final StringBuilder inputBuffer = new StringBuilder();
+    private int cursorPos;
+
+    // Conversation display
+    private final List<ConversationEntry> conversation = new ArrayList<>();
+    private int scrollOffset;
+
+    // LLM state
+    private LlmClient client;
+    private List<LlmClient.Message> messages;
+    private List<LlmClient.ToolDef> tools;
+    private AskTools askTools;
+    private final AtomicBoolean thinking = new AtomicBoolean();
+    private volatile Thread agentThread;
+    private String initError;
+
+    record ConversationEntry(String role, String text) {
+    }
+
+    void setContext(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    boolean isOpen() {
+        return visible;
+    }
+
+    int panelPercent() {
+        return SPLIT_PERCENTS[splitIndex];
+    }
+
+    void cycleHeight() {
+        splitIndex = (splitIndex + 1) % SPLIT_PERCENTS.length;
+    }
+
+    void open() {
+        visible = true;
+        if (client == null) {
+            initClient();
+        }
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    void destroy() {
+        close();
+        Thread t = agentThread;
+        if (t != null) {
+            t.interrupt();
+        }
+    }
+
+    private void initClient() {
+        try {
+            client = LlmClient.create()
+                    .withTemperature(0.3)
+                    .withTimeout(120)
+                    .withMaxTokens(4096);
+            if (!client.detectEndpoint()) {
+                initError = "No LLM service reachable. Set ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or start Ollama.";
+                client = null;
+                return;
+            }
+            initError = null;
+            messages = new ArrayList<>();
+            long pid = ctx != null && ctx.selectedPid != null ? 
Long.parseLong(ctx.selectedPid) : -1;
+            String name = ctx != null ? ctx.selectedName() : null;
+            askTools = new AskTools(pid);
+            tools = askTools.buildToolDefinitions();
+        } catch (Exception e) {
+            initError = "Failed to initialize AI: " + e.getMessage();
+            client = null;
+        }
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isKey(KeyCode.F8)) {
+            close();
+            return true;
+        }
+        if (ke.isKey(KeyCode.PAGE_UP)) {
+            scrollOffset += 5;
+            return true;
+        }
+        if (ke.isKey(KeyCode.PAGE_DOWN)) {
+            scrollOffset = Math.max(0, scrollOffset - 5);
+            return true;
+        }
+        if (thinking.get()) {
+            if (ke.isCtrlC()) {
+                Thread t = agentThread;
+                if (t != null) {
+                    t.interrupt();
+                }
+                thinking.set(false);
+                conversation.add(new ConversationEntry("system", 
"(cancelled)"));
+                return true;
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.ENTER)) {
+            if (!inputBuffer.isEmpty()) {
+                submitQuestion();
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.BACKSPACE)) {
+            if (cursorPos > 0) {
+                inputBuffer.deleteCharAt(cursorPos - 1);
+                cursorPos--;
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.DELETE)) {
+            if (cursorPos < inputBuffer.length()) {
+                inputBuffer.deleteCharAt(cursorPos);
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.LEFT)) {
+            if (cursorPos > 0) {
+                cursorPos--;
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.RIGHT)) {
+            if (cursorPos < inputBuffer.length()) {
+                cursorPos++;
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.HOME)) {
+            cursorPos = 0;
+            return true;
+        }
+        if (ke.isKey(KeyCode.END)) {
+            cursorPos = inputBuffer.length();
+            return true;
+        }
+        if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
+            inputBuffer.insert(cursorPos, ke.character());
+            cursorPos++;
+            return true;
+        }
+        return true;
+    }
+
+    private void submitQuestion() {
+        String question = inputBuffer.toString().trim();
+        inputBuffer.setLength(0);
+        cursorPos = 0;
+        scrollOffset = 0;
+
+        conversation.add(new ConversationEntry("user", question));
+        thinking.set(true);
+
+        // rebuild tools if target process changed
+        long pid = ctx != null && ctx.selectedPid != null ? 
Long.parseLong(ctx.selectedPid) : -1;
+        String name = ctx != null ? ctx.selectedName() : null;
+        askTools = new AskTools(pid);
+        tools = askTools.buildToolDefinitions();
+        String systemPrompt = AskTools.buildSystemPrompt(pid, name);
+
+        agentThread = new Thread(() -> {
+            try {
+                runAgentLoop(systemPrompt, question);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } catch (Exception e) {
+                conversation.add(new ConversationEntry("error", 
e.getMessage()));
+            } finally {
+                thinking.set(false);
+                agentThread = null;
+            }
+        }, "tui-ai-agent");
+        agentThread.setDaemon(true);
+        agentThread.start();
+    }
+
+    private void runAgentLoop(String systemPrompt, String question) throws 
InterruptedException {
+        if (messages == null) {
+            messages = new ArrayList<>();
+        }
+        messages.add(LlmClient.Message.user(question));
+
+        for (int i = 0; i < MAX_ITERATIONS; i++) {
+            if (Thread.interrupted()) {
+                throw new InterruptedException();
+            }
+
+            LlmClient.ChatResponse response = 
client.chatWithTools(systemPrompt, messages, tools);
+            if (response == null) {
+                conversation.add(new ConversationEntry("error", "No response 
from LLM"));
+                return;
+            }
+
+            // check for error response (null text, no tool calls, error stop 
reason)
+            if ("error".equals(response.stopReason())
+                    && (response.toolCalls() == null || 
response.toolCalls().isEmpty())
+                    && response.text() == null) {
+                conversation.add(new ConversationEntry("error", "LLM request 
failed. Check API key and endpoint."));
+                return;
+            }
+
+            if (response.toolCalls() != null && 
!response.toolCalls().isEmpty()) {
+                
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
response.toolCalls()));
+
+                List<LlmClient.ToolResult> results = new ArrayList<>();
+                for (LlmClient.ToolCall toolCall : response.toolCalls()) {
+                    if (Thread.interrupted()) {
+                        throw new InterruptedException();
+                    }
+                    String result = askTools.executeTool(toolCall.name(), 
toolCall.arguments());
+                    results.add(new LlmClient.ToolResult(toolCall.id(), 
result));
+                }
+                messages.add(LlmClient.Message.toolResults(results));
+            } else {
+                String text = response.text();
+                if (text != null && !text.isBlank()) {
+                    conversation.add(new ConversationEntry("assistant", text));
+                } else {
+                    conversation.add(new ConversationEntry("error", "Empty 
response from LLM."));
+                }
+                messages.add(LlmClient.Message.assistantWithToolCalls(text, 
List.of()));
+                return;
+            }
+        }
+        conversation.add(new ConversationEntry(
+                "error",
+                "Reached maximum iterations (" + MAX_ITERATIONS + ") without a 
final answer."));
+    }
+
+    void render(Frame frame, Rect area) {
+        Block block = Block.builder()
+                .borders(Borders.ALL)
+                .borderType(BorderType.ROUNDED)
+                .title(Title.from(Line.from(Span.styled(" AI ", 
Style.EMPTY.bold()))))
+                .build();
+        frame.renderWidget(block, area);
+        Rect inner = block.inner(area);
+        if (inner.height() < 2) {
+            return;
+        }
+
+        // Split inner area: conversation (fill) + separator (1 row) + input 
(1 row) + padding (1 row)
+        List<Rect> parts = Layout.vertical()
+                .constraints(Constraint.fill(), Constraint.length(1), 
Constraint.length(1), Constraint.length(1))
+                .split(inner);
+        Rect conversationArea = parts.get(0);
+        Rect separatorArea = parts.get(1);
+        Rect inputArea = parts.get(2);
+        // parts.get(3) is empty padding row above the bottom border
+
+        renderConversation(frame, conversationArea);
+        // horizontal line separator
+        String line = "─".repeat(separatorArea.width());
+        frame.renderWidget(Paragraph.from(Line.from(Span.styled(line, 
Style.EMPTY.dim()))),
+                separatorArea);
+        renderInput(frame, inputArea);
+    }
+
+    private void renderConversation(Frame frame, Rect area) {
+        if (area.height() < 1) {
+            return;
+        }
+
+        StringBuilder md = new StringBuilder();
+
+        if (initError != null) {
+            md.append("**Error:** ").append(initError).append("\n\n");
+        } else if (conversation.isEmpty() && !thinking.get()) {
+            md.append("*Ask a question about your Camel application...*\n");
+        }
+
+        for (ConversationEntry entry : conversation) {
+            switch (entry.role()) {
+                case "user" -> md.append("**You:** 
").append(entry.text()).append("\n\n");
+                case "assistant" -> md.append(entry.text()).append("\n\n");
+                case "error" -> md.append("**Error:** 
").append(entry.text()).append("\n\n");
+                case "system" -> 
md.append("*").append(entry.text()).append("*\n\n");
+                default -> {
+                }
+            }
+        }
+
+        if (thinking.get()) {
+            long dots = (System.currentTimeMillis() / 500) % 4;
+            md.append("*🤔 thinking").append(".".repeat((int) dots + 
1)).append("*\n");
+        }
+
+        MarkdownView view = MarkdownView.builder()
+                .source(md.toString())
+                .scroll(scrollOffset)
+                .build();
+        frame.renderWidget(view, area);
+    }
+
+    private void renderInput(Frame frame, Rect area) {
+        String prompt = "> ";
+        String text = inputBuffer.toString();
+
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.styled(prompt, Style.EMPTY.fg(Color.CYAN).bold()));
+
+        if (thinking.get()) {
+            spans.add(Span.styled(text, Style.EMPTY.dim()));
+        } else {
+            // Render with cursor
+            int maxWidth = area.width() - prompt.length();
+            if (maxWidth <= 0) {
+                return;
+            }
+            // Ensure cursor is visible by adjusting text window
+            int windowStart = 0;
+            if (cursorPos > maxWidth - 1) {
+                windowStart = cursorPos - maxWidth + 1;
+            }
+            String visible = text.substring(windowStart,
+                    Math.min(text.length(), windowStart + maxWidth));
+            int cursorInWindow = cursorPos - windowStart;
+
+            if (cursorInWindow >= 0 && cursorInWindow < visible.length()) {
+                spans.add(Span.raw(visible.substring(0, cursorInWindow)));
+                
spans.add(Span.styled(String.valueOf(visible.charAt(cursorInWindow)),
+                        Style.EMPTY.reversed()));
+                spans.add(Span.raw(visible.substring(cursorInWindow + 1)));
+            } else {
+                spans.add(Span.raw(visible));
+                if (cursorInWindow == visible.length()) {
+                    spans.add(Span.styled(" ", Style.EMPTY.reversed()));
+                }
+            }
+        }
+
+        frame.renderWidget(Paragraph.from(Line.from(spans)), area);
+    }
+
+    void renderFooter(List<Span> spans) {
+        MonitorContext.hint(spans, "F8", "close");
+        MonitorContext.hint(spans, "Shift+F8", panelPercent() + "%");
+        MonitorContext.hint(spans, "PgUp/Dn", "scroll");
+        if (!thinking.get()) {
+            MonitorContext.hint(spans, "Enter", "send");
+        } else {
+            MonitorContext.hint(spans, "Ctrl+C", "cancel");
+        }
+    }
+
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 2fac10ed617a..1be046368239 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -185,6 +185,7 @@ public class CamelMonitor extends CamelCommand {
     private final DrawOverlay drawOverlay = new DrawOverlay();
     private final HelpOverlay helpOverlay = new HelpOverlay();
     private final ShellPanel shellPanel = new ShellPanel();
+    private final AiPanel aiPanel = new AiPanel();
 
     private final ActionsPopup actionsPopup = new ActionsPopup(
             () -> data.get().stream()
@@ -280,6 +281,7 @@ public class CamelMonitor extends CamelCommand {
         actionsPopup.setContext(ctx);
         actionsPopup.setResetStatsAction(this::resetStats);
         shellPanel.setContext(ctx);
+        aiPanel.setContext(ctx);
         actionsPopup.setOpenShellAction(shellPanel::open);
         actionsPopup.setBrowseFilesAction(this::openFilesPopup);
         logTab = new LogTab(ctx);
@@ -347,6 +349,7 @@ public class CamelMonitor extends CamelCommand {
                     this::render);
         } finally {
             shellPanel.destroy();
+            aiPanel.destroy();
             if (mcpServer != null) {
                 mcpServer.stop();
             }
@@ -406,6 +409,13 @@ public class CamelMonitor extends CamelCommand {
                 }
                 return shellPanel.handleKeyEvent(ke);
             }
+            if (aiPanel.isOpen()) {
+                if (ke.isKey(KeyCode.F8) && ke.hasShift()) {
+                    aiPanel.cycleHeight();
+                    return true;
+                }
+                return aiPanel.handleKeyEvent(ke);
+            }
             if (actionsPopup.isVisible()) {
                 return actionsPopup.handleKeyEvent(ke);
             }
@@ -641,10 +651,24 @@ public class CamelMonitor extends CamelCommand {
             if (shellPanel.isOpen()) {
                 shellPanel.close();
             } else {
+                if (aiPanel.isOpen()) {
+                    aiPanel.close();
+                }
                 shellPanel.open();
             }
             return true;
         }
+        if (ke.isKey(KeyCode.F8) && mcp) {
+            if (aiPanel.isOpen()) {
+                aiPanel.close();
+            } else {
+                if (shellPanel.isOpen()) {
+                    shellPanel.close();
+                }
+                aiPanel.open();
+            }
+            return true;
+        }
         if (ke.isKey(KeyCode.F2)) {
             if (tabsState.selected() == TAB_ROUTES && routesTab != null) {
                 
actionsPopup.setPreSelectedRouteId(routesTab.selectedRouteId());
@@ -1002,7 +1026,8 @@ public class CamelMonitor extends CamelCommand {
         renderHeader(frame, mainChunks.get(0));
         renderTabs(frame, mainChunks.get(1));
         Rect contentArea = mainChunks.get(2);
-        ctx.shellPercent = shellPanel.isOpen() ? shellPanel.panelPercent() : 0;
+        ctx.shellPercent = shellPanel.isOpen() ? shellPanel.panelPercent()
+                : aiPanel.isOpen() ? aiPanel.panelPercent() : 0;
         if (shellPanel.isOpen()) {
             List<Rect> splitChunks = Layout.vertical()
                     .constraints(Constraint.percentage(100 - 
shellPanel.panelPercent()),
@@ -1010,6 +1035,13 @@ public class CamelMonitor extends CamelCommand {
                     .split(contentArea);
             renderContent(frame, splitChunks.get(0));
             shellPanel.render(frame, splitChunks.get(1));
+        } else if (aiPanel.isOpen()) {
+            List<Rect> splitChunks = Layout.vertical()
+                    .constraints(Constraint.percentage(100 - 
aiPanel.panelPercent()),
+                            Constraint.percentage(aiPanel.panelPercent()))
+                    .split(contentArea);
+            renderContent(frame, splitChunks.get(0));
+            aiPanel.render(frame, splitChunks.get(1));
         } else {
             renderContent(frame, contentArea);
         }
@@ -1763,6 +1795,8 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "Esc", "close");
         } else if (shellPanel.isOpen()) {
             shellPanel.renderFooter(spans);
+        } else if (aiPanel.isOpen()) {
+            aiPanel.renderFooter(spans);
         } else {
             MonitorTab tab = activeTab();
 
@@ -1857,6 +1891,9 @@ public class CamelMonitor extends CamelCommand {
             hint(fKeySpans, "F3", "switch");
         }
         hint(fKeySpans, "F6", "shell");
+        if (mcp) {
+            hint(fKeySpans, "F8", "AI");
+        }
         spans.addAll(insertPos, fKeySpans);
         // Return total F-key span count. The footer drop loop uses this to 
remove pairs from
         // the tail (F6, then F3, F2), stopping before the first pair (F1 help 
when present).

Reply via email to