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

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


The following commit(s) were added to refs/heads/main by this push:
     new 2cc56ca89262 CAMEL-23855: Add F8 AI prompt panel to TUI
2cc56ca89262 is described below

commit 2cc56ca892621343acda2c5f18ed90ed088b1995
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 29 19:56:37 2026 +0200

    CAMEL-23855: Add F8 AI prompt panel to TUI
    
    Add F8 AI prompt panel to TUI for interactive AI assistant that works
    with or without --mcp. The panel supports split view (25%/50%/75%)
    with Shift+F8 to cycle, auto-scroll, dimmed elapsed time display,
    and an AI Log popup showing tool calls and results.
    
    Also adds 10 missing MCP tools to AskTools (get_memory, get_errors,
    get_history, send_message, eval_expression, browse_endpoint, etc.),
    fixes shell scrollback by accessing JLine's private history field,
    simplifies scroll to plain PgUp/PgDn, removes spacer rows between
    History tab panels, and adds comprehensive TUI test coverage.
    
    Closes #24321
    
    Co-Authored-By: Claude <[email protected]>
---
 .../apache/camel/dsl/jbang/core/commands/Ask.java  | 836 +--------------------
 .../core/commands/{Ask.java => AskTools.java}      | 615 ++++++---------
 .../camel/dsl/jbang/core/commands/LlmClient.java   |  64 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  33 +-
 .../dsl/jbang/core/commands/tui/AiLogPopup.java    | 222 ++++++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 564 ++++++++++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  38 +-
 .../dsl/jbang/core/commands/tui/HistoryTab.java    |  12 +-
 .../dsl/jbang/core/commands/tui/ShellPanel.java    |  23 +-
 9 files changed, 1133 insertions(+), 1274 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..05a89ddddbe4 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);
-    }
-
-    @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 AskTools(long targetPid) {
+        this.targetPid = 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;
+    public long getTargetPid() {
+        return targetPid;
     }
 
-    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.",
@@ -369,8 +112,27 @@ public class Ask extends CamelCommand {
                 "get_properties",
                 "Show configuration properties of the running Camel 
application.",
                 emptyParams()));
+        tools.add(new LlmClient.ToolDef(
+                "get_memory",
+                "Show JVM memory usage (heap/non-heap), garbage collection 
stats, and thread counts.",
+                emptyParams()));
+        tools.add(new LlmClient.ToolDef(
+                "get_errors",
+                "Get captured routing errors from the running Camel 
application. Returns error details including exception, exchange context, and 
route information.",
+                emptyParams()));
+        tools.add(new LlmClient.ToolDef(
+                "get_history",
+                "Get the message history trace of the last completed exchange. 
Shows the route path, processors visited, headers, body, and timing.",
+                emptyParams()));
+        tools.add(new LlmClient.ToolDef(
+                "get_variables",
+                "Show exchange variables in the Camel context.",
+                emptyParams()));
+        tools.add(new LlmClient.ToolDef(
+                "get_services",
+                "Show services registered in the Camel service registry.",
+                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).",
@@ -391,13 +153,40 @@ public class Ask extends CamelCommand {
                 "get_top_processors",
                 "Show top processor statistics: which processors are slowest 
and most active.",
                 emptyParams()));
+        tools.add(new LlmClient.ToolDef(
+                "get_route_topology",
+                "Get the inter-route topology showing how routes connect to 
each other and to external endpoints.",
+                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(
+                "send_message",
+                "Send a test message to a Camel endpoint in the running 
application.",
+                objectParams(Map.of(
+                        "endpoint", stringProp("Endpoint URI to send to (e.g., 
direct:myRoute, seda:queue)"),
+                        "body", stringProp("Message body to send"),
+                        "headers", stringProp("Message headers as key=value 
pairs separated by newlines")))));
+        tools.add(new LlmClient.ToolDef(
+                "eval_expression",
+                "Evaluate a Camel expression in the given language (e.g., 
simple, jsonpath, xpath) against the running context.",
+                objectParams(Map.of(
+                        "language", stringProp("Expression language (e.g., 
simple, jsonpath, xpath, jq)"),
+                        "expression", stringProp("Expression to evaluate")))));
+        tools.add(new LlmClient.ToolDef(
+                "browse_endpoint",
+                "Browse messages in a Camel endpoint (e.g., browse messages 
queued in a SEDA endpoint).",
+                objectParams(Map.of(
+                        "endpoint", stringProp("Endpoint URI to browse (e.g., 
seda:queue)"),
+                        "limit", stringProp("Maximum number of messages to 
return (default: 50)")))));
+        tools.add(new LlmClient.ToolDef(
+                "get_thread_dump",
+                "Get a JVM thread dump showing thread names, states, and stack 
traces.",
+                emptyParams()));
+
         tools.add(new LlmClient.ToolDef(
                 "stop_route",
                 "Gracefully stop a route. The route will finish processing 
in-flight exchanges before stopping.",
@@ -419,13 +208,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 +230,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 +243,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 +260,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 +282,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" ->
@@ -520,29 +303,43 @@ public class Ask extends CamelCommand {
                     targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "consumers");
                 case "get_properties" ->
                     targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "properties");
+                case "get_memory" ->
+                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "memory");
+                case "get_errors" -> targetPid < 0 ? NO_PROCESS : 
executeGetErrors();
+                case "get_history" -> targetPid < 0 ? NO_PROCESS : 
executeGetHistory();
+                case "get_variables" ->
+                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "variables");
+                case "get_services" ->
+                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.readStatusSection(targetPid, "services");
                 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 "get_route_topology" ->
+                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.executeAction(targetPid, "route-topology", root -> {
+                        root.put("metric", "true");
+                        root.put("external", "true");
+                    });
                 case "trace_control" -> targetPid < 0 ? NO_PROCESS : 
executeTraceControl(args);
+                case "send_message" -> targetPid < 0 ? NO_PROCESS : 
executeSendMessage(args);
+                case "eval_expression" -> targetPid < 0 ? NO_PROCESS : 
executeEvalExpression(args);
+                case "browse_endpoint" -> targetPid < 0 ? NO_PROCESS : 
executeBrowseEndpoint(args);
+                case "get_thread_dump" ->
+                    targetPid < 0 ? NO_PROCESS : 
RuntimeHelper.executeAction(targetPid, "thread-dump", null);
                 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);
@@ -553,6 +350,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()) {
@@ -641,15 +480,79 @@ public class Ask extends CamelCommand {
         });
     }
 
+    private String executeGetErrors() {
+        JsonObject errors = RuntimeHelper.readErrorFile(targetPid);
+        if (errors == null) {
+            return "No errors captured.";
+        }
+        return errors.toJson();
+    }
+
+    private String executeGetHistory() {
+        JsonObject history = RuntimeHelper.readHistoryFile(targetPid);
+        if (history == null) {
+            return "No message history available.";
+        }
+        return history.toJson();
+    }
+
+    private String executeSendMessage(JsonObject args) {
+        String endpoint = args.getString("endpoint");
+        if (endpoint == null || endpoint.isBlank()) {
+            return "Error: endpoint is required";
+        }
+        String body = args.getString("body");
+        String headers = args.getString("headers");
+        JsonObject result = RuntimeHelper.sendMessage(targetPid, endpoint, 
body, headers);
+        return result.toJson();
+    }
+
+    private String executeEvalExpression(JsonObject args) {
+        String language = args.getString("language");
+        String expression = args.getString("expression");
+        if (language == null || language.isBlank()) {
+            return "Error: language is required";
+        }
+        if (expression == null || expression.isBlank()) {
+            return "Error: expression is required";
+        }
+        return RuntimeHelper.executeAction(targetPid, "eval", root -> {
+            root.put("language", language);
+            root.put("predicate", "false");
+            root.put("template", Jsoner.escape(expression));
+        });
+    }
+
+    private String executeBrowseEndpoint(JsonObject args) {
+        String endpoint = args.getString("endpoint");
+        if (endpoint == null || endpoint.isBlank()) {
+            return "Error: endpoint is required";
+        }
+        String limitStr = args.getString("limit");
+        int limit = 50;
+        if (limitStr != null && !limitStr.isBlank()) {
+            try {
+                limit = Integer.parseInt(limitStr);
+            } catch (NumberFormatException e) {
+                // use default
+            }
+        }
+        int browseLimit = limit;
+        return RuntimeHelper.executeAction(targetPid, "browse", root -> {
+            root.put("filter", endpoint);
+            root.put("limit", browseLimit);
+        });
+    }
+
     // ---- Catalog tools ----
 
     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 +579,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 +615,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 +638,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 +645,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 +687,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 +736,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 +753,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 +761,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 +819,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 +844,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 +852,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 +868,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 +955,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 +976,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/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 6f57ad1ec389..d90a0efa92eb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -87,11 +87,12 @@ class ActionsPopup {
         SETUP_AI,
         MCP_INFO,
         MCP_LOG,
+        AI_LOG,
         SHELL
     }
 
     private static final int[] GROUP_SIZES = { 5, 4, 5 };
-    private static final int MCP_GROUP_SIZE = 3;
+    private static final int MCP_GROUP_SIZE = 4;
     private static final int SHELL_GROUP_SIZE = 1;
 
     private final Supplier<Set<String>> runningNames;
@@ -148,6 +149,7 @@ class ActionsPopup {
     private String selectedFolder;
 
     private final McpLogPopup mcpLogPopup = new McpLogPopup();
+    private final AiLogPopup aiLogPopup = new AiLogPopup();
 
     private final DoctorPopup doctorPopup = new DoctorPopup();
     private final SendMessagePopup sendMessagePopup = new SendMessagePopup();
@@ -218,6 +220,10 @@ class ActionsPopup {
         mcpLogPopup.setActivityLog(activityLog);
     }
 
+    void setAiActivityLog(Supplier<List<AiPanel.LogEntry>> activityLog) {
+        aiLogPopup.setActivityLog(activityLog);
+    }
+
     private int visualActionCount() {
         int total = 0;
         for (int gs : GROUP_SIZES) {
@@ -273,7 +279,7 @@ class ActionsPopup {
                 Action.SHOW_KEYSTROKES));
         if (mcpEnabled) {
             flat.add(null);
-            flat.addAll(List.of(Action.SETUP_AI, Action.MCP_INFO, 
Action.MCP_LOG));
+            flat.addAll(List.of(Action.SETUP_AI, Action.MCP_INFO, 
Action.MCP_LOG, Action.AI_LOG));
         }
         flat.add(null);
         flat.add(Action.SHELL);
@@ -298,7 +304,7 @@ class ActionsPopup {
         return showActionsMenu || showExampleBrowser || showFolderInput || 
runOptionsForm.isVisible()
                 || showDocPicker || showDocViewer
                 || showInfraBrowser || showInfraPortDialog
-                || mcpLogPopup.isVisible() || doctorPopup.isVisible()
+                || mcpLogPopup.isVisible() || aiLogPopup.isVisible() || 
doctorPopup.isVisible()
                 || sendMessagePopup.isVisible() || stopAllPopup.isVisible() || 
captionOverlay.isInlineMode();
     }
 
@@ -366,6 +372,7 @@ class ActionsPopup {
             labels.add("Setup AI...");
             labels.add("MCP Info");
             labels.add("MCP Log");
+            labels.add("AI Log");
         }
         labels.add("───");
         labels.add("Shell");
@@ -387,6 +394,7 @@ class ActionsPopup {
         showInfraBrowser = false;
         showInfraPortDialog = false;
         mcpLogPopup.close();
+        aiLogPopup.close();
         doctorPopup.close();
         sendMessagePopup.close();
         stopAllPopup.close();
@@ -419,6 +427,9 @@ class ActionsPopup {
         if (mcpLogPopup.handleKeyEvent(ke)) {
             return true;
         }
+        if (aiLogPopup.handleKeyEvent(ke)) {
+            return true;
+        }
         if (showDocViewer) {
             if (ke.isCancel()) {
                 showDocViewer = false;
@@ -608,6 +619,9 @@ class ActionsPopup {
                     } else if (action == Action.MCP_LOG) {
                         showActionsMenu = false;
                         openMcpLog();
+                    } else if (action == Action.AI_LOG) {
+                        showActionsMenu = false;
+                        openAiLog();
                     } else if (action == Action.SEND_MESSAGE) {
                         showActionsMenu = false;
                         openSendMessage();
@@ -664,6 +678,9 @@ class ActionsPopup {
         if (mcpLogPopup.isVisible()) {
             mcpLogPopup.render(frame, area);
         }
+        if (aiLogPopup.isVisible()) {
+            aiLogPopup.render(frame, area);
+        }
         if (doctorPopup.isVisible()) {
             doctorPopup.render(frame, area);
         }
@@ -695,6 +712,10 @@ class ActionsPopup {
             doctorPopup.renderFooter(spans);
             return;
         }
+        if (aiLogPopup.isVisible()) {
+            aiLogPopup.renderFooter(spans);
+            return;
+        }
         if (mcpLogPopup.isVisible()) {
             mcpLogPopup.renderFooter(spans);
             return;
@@ -809,6 +830,7 @@ class ActionsPopup {
             items.add(ListItem.from("  🧠 Setup AI..."));
             items.add(ListItem.from("  🤖 MCP Info"));
             items.add(ListItem.from("  📋 MCP Log"));
+            items.add(ListItem.from("  💬 AI Log"));
         }
         // Group 5: Shell
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
@@ -1245,6 +1267,10 @@ class ActionsPopup {
         mcpLogPopup.open();
     }
 
+    private void openAiLog() {
+        aiLogPopup.open();
+    }
+
     // ---- Folder Input ----
 
     private void openFolderInput() {
@@ -2171,6 +2197,7 @@ class ActionsPopup {
             case SETUP_AI -> openSetupAI();
             case MCP_INFO -> openMcpInfo();
             case MCP_LOG -> openMcpLog();
+            case AI_LOG -> openAiLog();
             default -> {
                 return false;
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
new file mode 100644
index 000000000000..147c75a51a69
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
@@ -0,0 +1,222 @@
+/*
+ * 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.function.Supplier;
+
+import dev.tamboui.layout.Rect;
+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.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+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.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.util.json.Jsoner;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class AiLogPopup {
+
+    private boolean visible;
+    private Supplier<List<AiPanel.LogEntry>> activityLog;
+    private List<AiPanel.LogEntry> entries;
+    private int selected;
+    private int detailScroll;
+
+    void setActivityLog(Supplier<List<AiPanel.LogEntry>> activityLog) {
+        this.activityLog = activityLog;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open() {
+        entries = activityLog != null ? activityLog.get() : List.of();
+        selected = entries.isEmpty() ? 0 : entries.size() - 1;
+        detailScroll = 0;
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+        } else if (ke.isUp() || ke.isChar('k')) {
+            if (entries != null && !entries.isEmpty()) {
+                selected = Math.max(0, selected - 1);
+                detailScroll = 0;
+            }
+        } else if (ke.isDown() || ke.isChar('j')) {
+            if (entries != null && !entries.isEmpty()) {
+                selected = Math.min(entries.size() - 1, selected + 1);
+                detailScroll = 0;
+            }
+        } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            detailScroll = Math.max(0, detailScroll - 5);
+        } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            detailScroll += 5;
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() - 
4, area.height() - 2);
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        if (entries == null || entries.isEmpty()) {
+            Block block = Block.builder()
+                    .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                    .title(" AI Log ")
+                    .titleBottom(Title.from(Line.from(
+                            Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+                    .build();
+            frame.renderWidget(block, popup);
+            Rect inner = block.inner(popup);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled("No AI activity yet. Open the AI panel (F8) 
and ask a question.", Style.EMPTY.dim()))),
+                    inner);
+            return;
+        }
+
+        int splitY = popup.top() + Math.max(3, (popup.height() * 2) / 5);
+        Rect masterArea = new Rect(popup.left(), popup.top(), popup.width(), 
splitY - popup.top());
+        Rect detailArea = new Rect(popup.left(), splitY, popup.width(), 
popup.bottom() - splitY);
+
+        renderMaster(frame, masterArea);
+        renderDetail(frame, detailArea);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "↑↓", "select");
+        hint(spans, "PgUp/Dn", "scroll detail");
+        hintLast(spans, "Esc", "back");
+    }
+
+    private void renderMaster(Frame frame, Rect area) {
+        List<ListItem> items = new ArrayList<>();
+        for (AiPanel.LogEntry entry : entries) {
+            Style levelStyle = switch (entry.level()) {
+                case QUESTION -> Style.EMPTY.fg(Color.CYAN);
+                case TOOL -> Style.EMPTY.fg(Color.YELLOW);
+                case RESULT -> Style.EMPTY.fg(Color.GREEN);
+                case RESPONSE -> Style.EMPTY.fg(Color.MAGENTA);
+                case ERROR -> Style.EMPTY.fg(Color.LIGHT_RED);
+            };
+            String levelTag = switch (entry.level()) {
+                case QUESTION -> " ASK      ";
+                case TOOL -> " TOOL     ";
+                case RESULT -> " RESULT   ";
+                case RESPONSE -> " RESPONSE ";
+                case ERROR -> " ERROR    ";
+            };
+            items.add(ListItem.from(Line.from(
+                    Span.styled(entry.timestamp(), Style.EMPTY.dim()),
+                    Span.styled(levelTag, levelStyle),
+                    Span.raw(entry.message()))));
+        }
+
+        ListState masterState = new ListState();
+        masterState.select(selected);
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("▸ ")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" AI Log ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, area, masterState);
+    }
+
+    private void renderDetail(Frame frame, Rect area) {
+        AiPanel.LogEntry entry = entries.get(selected);
+        List<Line> lines = new ArrayList<>();
+
+        String detail = entry.detail();
+        if (detail != null && !detail.isBlank()) {
+            if (entry.level() == AiPanel.LogLevel.TOOL || entry.level() == 
AiPanel.LogLevel.RESULT) {
+                lines.add(Line.from(Span.styled(
+                        entry.level() == AiPanel.LogLevel.TOOL ? "▶ Arguments" 
: "◀ Result",
+                        Style.EMPTY.fg(entry.level() == AiPanel.LogLevel.TOOL 
? Color.YELLOW : Color.GREEN).bold())));
+                addJsonLines(lines, detail);
+            } else {
+                lines.add(Line.from(Span.styled("▶ Content",
+                        Style.EMPTY.fg(Color.CYAN).bold())));
+                for (String line : detail.split("\n", -1)) {
+                    lines.add(Line.from(Span.styled("  " + line, 
Style.EMPTY.dim())));
+                }
+            }
+        } else {
+            lines.add(Line.from(Span.styled("(no detail data)", 
Style.EMPTY.dim())));
+        }
+
+        Block detailBlock = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .title(" Detail ")
+                .build();
+        frame.renderWidget(detailBlock, area);
+        Rect inner = detailBlock.inner(area);
+
+        int visibleLines = inner.height();
+        int totalLines = lines.size();
+        int clampedScroll = Math.min(detailScroll, Math.max(0, totalLines - 
visibleLines));
+        int end = Math.min(clampedScroll + visibleLines, totalLines);
+        if (clampedScroll < end) {
+            List<Line> visible = lines.subList(clampedScroll, end);
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
+                    inner);
+        }
+    }
+
+    private static void addJsonLines(List<Line> lines, String json) {
+        try {
+            String pretty = Jsoner.prettyPrint(json, 2);
+            for (String line : pretty.split("\n", -1)) {
+                lines.add(Line.from(Span.styled("  " + line, 
Style.EMPTY.dim())));
+            }
+        } catch (Exception e) {
+            for (String line : json.split("\n", -1)) {
+                lines.add(Line.from(Span.styled("  " + line, 
Style.EMPTY.dim())));
+            }
+        }
+    }
+}
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..62cb48158807
--- /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,564 @@
+/*
+ * 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.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+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 static final int MAX_LOG_ENTRIES = 200;
+    private static final DateTimeFormatter TIME_FMT
+            = 
DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    enum LogLevel {
+        QUESTION,
+        TOOL,
+        RESULT,
+        RESPONSE,
+        ERROR
+    }
+
+    record LogEntry(String timestamp, LogLevel level, String message, String 
detail) {
+    }
+
+    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;
+    private long thinkingStartTime;
+
+    // Activity log for AI Log popup
+    private final List<LogEntry> activityLog = new ArrayList<>();
+
+    record ConversationEntry(String role, String text, long elapsedSeconds) {
+        ConversationEntry(String role, String text) {
+            this(role, text, -1);
+        }
+    }
+
+    void setContext(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    synchronized List<LogEntry> getActivityLog() {
+        return new ArrayList<>(activityLog);
+    }
+
+    private synchronized void log(LogLevel level, String message, String 
detail) {
+        activityLog.add(new LogEntry(TIME_FMT.format(Instant.now()), level, 
message, detail));
+        if (activityLog.size() > MAX_LOG_ENTRIES) {
+            activityLog.remove(0);
+        }
+    }
+
+    boolean isOpen() {
+        return visible;
+    }
+
+    int panelPercent() {
+        return SPLIT_PERCENTS[splitIndex];
+    }
+
+    private long lastResponseElapsed() {
+        if (thinking.get() || conversation.isEmpty()) {
+            return -1;
+        }
+        ConversationEntry last = conversation.get(conversation.size() - 1);
+        return "assistant".equals(last.role()) ? last.elapsedSeconds() : -1;
+    }
+
+    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));
+        log(LogLevel.QUESTION, "Question", question);
+        thinkingStartTime = System.currentTimeMillis();
+        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) {
+                String err = "No response from LLM";
+                conversation.add(new ConversationEntry("error", err));
+                log(LogLevel.ERROR, "Error", err);
+                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) {
+                String err = "LLM request failed. Check API key and endpoint.";
+                conversation.add(new ConversationEntry("error", err));
+                log(LogLevel.ERROR, "Error", err);
+                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();
+                    }
+                    log(LogLevel.TOOL, toolCall.name(), 
toolCall.arguments().toJson());
+                    String result = askTools.executeTool(toolCall.name(), 
toolCall.arguments());
+                    log(LogLevel.RESULT, toolCall.name(), result);
+                    results.add(new LlmClient.ToolResult(toolCall.id(), 
result));
+                }
+                messages.add(LlmClient.Message.toolResults(results));
+            } else {
+                String text = response.text();
+                if (text != null && !text.isBlank()) {
+                    long elapsed = (System.currentTimeMillis() - 
thinkingStartTime) / 1000;
+                    conversation.add(new ConversationEntry("assistant", text, 
elapsed));
+                    log(LogLevel.RESPONSE, "Response (" + elapsed + "s)", 
text);
+                } else {
+                    String err = "Empty response from LLM.";
+                    conversation.add(new ConversationEntry("error", err));
+                    log(LogLevel.ERROR, "Error", err);
+                }
+                scrollOffset = 0;
+                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) {
+        // At 25% show elapsed in the title bar to save space
+        long titleElapsed = lastResponseElapsed();
+        Line titleLine;
+        if (splitIndex == 0 && titleElapsed >= 0) {
+            titleLine = Line.from(
+                    Span.styled(" AI ", Style.EMPTY.bold()),
+                    Span.styled("(" + titleElapsed + "s) ", 
Style.EMPTY.dim()));
+        } else {
+            titleLine = Line.from(Span.styled(" AI ", Style.EMPTY.bold()));
+        }
+
+        Block block = Block.builder()
+                .borders(Borders.ALL)
+                .borderType(BorderType.ROUNDED)
+                .title(Title.from(titleLine))
+                .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)
+        List<Rect> parts = Layout.vertical()
+                .constraints(Constraint.fill(), Constraint.length(1), 
Constraint.length(1))
+                .split(inner);
+        Rect conversationArea = parts.get(0);
+        Rect separatorArea = parts.get(1);
+        Rect inputArea = parts.get(2);
+
+        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()) {
+            frame.renderWidget(
+                    Paragraph.from(Line.from(Span.styled("Ask a question about 
your Camel application...", Style.EMPTY.dim()))),
+                    area);
+            return;
+        }
+
+        for (ConversationEntry entry : conversation) {
+            switch (entry.role()) {
+                case "user" -> md.append("**You:** 
").append(entry.text()).append("\n\n");
+                case "assistant" -> 
md.append(toHardBreaks(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 elapsed = (System.currentTimeMillis() - thinkingStartTime) / 
1000;
+            long dots = (System.currentTimeMillis() / 500) % 4;
+            md.append("*🤔 thinking");
+            if (elapsed > 0) {
+                md.append(" (").append(elapsed).append("s)");
+            }
+            md.append(".".repeat((int) dots + 1)).append("*\n");
+        }
+
+        // Show elapsed time as a dimmed line below the markdown when at the 
bottom
+        long lastElapsed = -1;
+        if (!thinking.get() && !conversation.isEmpty()) {
+            ConversationEntry last = conversation.get(conversation.size() - 1);
+            if ("assistant".equals(last.role()) && last.elapsedSeconds() >= 0) 
{
+                lastElapsed = last.elapsedSeconds();
+            }
+        }
+
+        // Reserve 1 row for dimmed elapsed time (skip at 25% — shown in title 
bar instead)
+        Rect mdArea = area;
+        Rect elapsedArea = null;
+        if (lastElapsed >= 0 && splitIndex > 0 && area.height() > 2) {
+            List<Rect> vParts = Layout.vertical()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(area);
+            mdArea = vParts.get(0);
+            elapsedArea = vParts.get(1);
+        }
+
+        String source = md.toString();
+
+        // Estimate total rendered lines (accounting for word wrap)
+        int contentWidth = Math.max(1, mdArea.width());
+        int estimatedLines = 0;
+        for (String l : source.split("\n", -1)) {
+            estimatedLines += Math.max(1, (l.length() / contentWidth) + 1);
+        }
+
+        boolean overflow = estimatedLines > mdArea.height();
+        Rect contentArea = mdArea;
+        Rect scrollbarArea = null;
+        if (overflow) {
+            List<Rect> hParts = Layout.horizontal()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(mdArea);
+            contentArea = hParts.get(0);
+            scrollbarArea = hParts.get(1);
+        }
+
+        // scrollOffset=0 means auto-scroll to bottom (most recent content 
visible)
+        // scrollOffset>0 means user scrolled up by that many lines
+        // Clamp so PgDn always has immediate effect after scrolling past the 
top
+        int maxScrollOffset = Math.max(0, estimatedLines - 
contentArea.height());
+        scrollOffset = Math.min(scrollOffset, maxScrollOffset);
+
+        int scroll = Math.max(0, maxScrollOffset - scrollOffset);
+
+        MarkdownView view = MarkdownView.builder()
+                .source(source)
+                .scroll(scroll)
+                .build();
+        frame.renderWidget(view, contentArea);
+
+        if (overflow && scrollbarArea != null) {
+            renderScrollbar(frame, scrollbarArea, estimatedLines, 
contentArea.height(), scroll);
+        }
+
+        if (elapsedArea != null && lastElapsed >= 0) {
+            frame.renderWidget(
+                    Paragraph.from(Line.from(Span.styled("(" + lastElapsed + 
"s)", Style.EMPTY.dim()))),
+                    elapsedArea);
+        }
+    }
+
+    private void renderInput(Frame frame, Rect area) {
+        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");
+        }
+    }
+
+    private void renderScrollbar(Frame frame, Rect area, int totalLines, int 
visibleHeight, int scroll) {
+        int thumbSize = Math.max(1, visibleHeight * visibleHeight / 
Math.max(1, totalLines));
+        int maxScroll = Math.max(1, totalLines - visibleHeight);
+        int thumbPos = (int) ((long) Math.min(scroll, maxScroll) * 
(visibleHeight - thumbSize) / maxScroll);
+
+        List<Line> lines = new ArrayList<>();
+        for (int i = 0; i < area.height(); i++) {
+            if (i >= thumbPos && i < thumbPos + thumbSize) {
+                lines.add(Line.from(Span.styled("▐", 
Style.EMPTY.fg(Color.CYAN))));
+            } else {
+                lines.add(Line.from(Span.styled("│", Style.EMPTY.dim())));
+            }
+        }
+        frame.renderWidget(Paragraph.from(new dev.tamboui.text.Text(lines, 
dev.tamboui.layout.Alignment.LEFT)), area);
+    }
+
+    private static String toHardBreaks(String text) {
+        if (text == null) {
+            return "";
+        }
+        // Convert single newlines to markdown hard breaks (two trailing 
spaces + newline)
+        // so the LLM's line-by-line formatting is preserved in MarkdownView.
+        // Double newlines (paragraph breaks) are left as-is.
+        return text.replaceAll("(?<!\n)\n(?!\n)", "  \n");
+    }
+
+}
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..dea72389ce84 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);
@@ -317,6 +319,7 @@ public class CamelMonitor extends CamelCommand {
 
         eventLog = new TuiEventLog(500);
         Path mcpJsonFile = null;
+        actionsPopup.setAiActivityLog(aiPanel::getActivityLog);
         if (mcp) {
             mcpServer = new TuiMcpServer(mcpPort, this);
             try {
@@ -347,6 +350,7 @@ public class CamelMonitor extends CamelCommand {
                     this::render);
         } finally {
             shellPanel.destroy();
+            aiPanel.destroy();
             if (mcpServer != null) {
                 mcpServer.stop();
             }
@@ -406,6 +410,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 +652,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)) {
+            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 +1027,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 +1036,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 +1796,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 +1892,7 @@ public class CamelMonitor extends CamelCommand {
             hint(fKeySpans, "F3", "switch");
         }
         hint(fKeySpans, "F6", "shell");
+        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).
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 737449f5d37f..1b4abc00bf92 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -1078,7 +1078,7 @@ class HistoryTab implements MonitorTab {
         List<TraceEntry> steps = 
getTraceStepsDepthFirst(traceSelectedExchangeId);
 
         List<Rect> chunks = Layout.vertical()
-                .constraints(Constraint.length(10), Constraint.length(1), 
Constraint.fill())
+                .constraints(Constraint.length(10), Constraint.fill())
                 .split(area);
 
         Map<String, String> descMap = showDescription ? getRouteDescriptions() 
: Collections.emptyMap();
@@ -1099,10 +1099,10 @@ class HistoryTab implements MonitorTab {
 
         if (showWaterfall) {
             Integer sel = traceStepTableState.selected();
-            renderWaterfall(frame, chunks.get(2), 
steps.stream().map(WaterfallStep::fromTrace).toList(),
+            renderWaterfall(frame, chunks.get(1), 
steps.stream().map(WaterfallStep::fromTrace).toList(),
                     sel != null ? sel : -1);
         } else {
-            renderTraceStepDetail(frame, chunks.get(2), steps);
+            renderTraceStepDetail(frame, chunks.get(1), steps);
         }
     }
 
@@ -1348,7 +1348,7 @@ class HistoryTab implements MonitorTab {
         List<HistoryEntry> current = reorderHistoryDepthFirst(historyEntries);
 
         List<Rect> chunks = Layout.vertical()
-                .constraints(Constraint.length(10), Constraint.length(1), 
Constraint.fill())
+                .constraints(Constraint.length(10), Constraint.fill())
                 .split(area);
 
         Map<String, String> descMap = showDescription ? getRouteDescriptions() 
: Collections.emptyMap();
@@ -1369,10 +1369,10 @@ class HistoryTab implements MonitorTab {
 
         if (showWaterfall) {
             Integer sel = historyTableState.selected();
-            renderWaterfall(frame, chunks.get(2), 
current.stream().map(WaterfallStep::fromHistory).toList(),
+            renderWaterfall(frame, chunks.get(1), 
current.stream().map(WaterfallStep::fromHistory).toList(),
                     sel != null ? sel : -1);
         } else {
-            renderHistoryDetail(frame, chunks.get(2), current);
+            renderHistoryDetail(frame, chunks.get(1), current);
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index fbdee894f0e0..d8a0f98d47bf 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -18,7 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.lang.reflect.Method;
+import java.lang.reflect.Field;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -138,13 +138,13 @@ class ShellPanel {
             return true;
         }
 
-        // Shift+PageUp/Down for scrollback through history
-        if (ke.isKey(KeyCode.PAGE_UP) && ke.hasShift()) {
+        // PageUp/Down for scrollback through history
+        if (ke.isKey(KeyCode.PAGE_UP)) {
             int histSize = screenTerminal != null ? 
getHistorySize(screenTerminal) : 0;
             scrollOffset = Math.min(scrollOffset + lastHeight, histSize);
             return true;
         }
-        if (ke.isKey(KeyCode.PAGE_DOWN) && ke.hasShift()) {
+        if (ke.isKey(KeyCode.PAGE_DOWN)) {
             scrollOffset = Math.max(0, scrollOffset - lastHeight);
             return true;
         }
@@ -280,7 +280,7 @@ class ShellPanel {
     void renderFooter(List<Span> spans) {
         MonitorContext.hint(spans, "F6", "close");
         MonitorContext.hint(spans, "Shift+F6", SPLIT_PERCENTS[splitIndex] + 
"%");
-        MonitorContext.hint(spans, "Shift+PgUp/Dn", "scroll");
+        MonitorContext.hint(spans, "PgUp/Dn", "scroll");
     }
 
     private List<Line> renderLiveView(long[] screen, int width, int height) {
@@ -619,20 +619,17 @@ class ShellPanel {
     @SuppressWarnings("unchecked")
     private static List<long[]> getHistory(ScreenTerminal st) {
         try {
-            Method m = ScreenTerminal.class.getMethod("getHistory");
-            return (List<long[]>) m.invoke(st);
+            Field f = ScreenTerminal.class.getDeclaredField("history");
+            f.setAccessible(true);
+            List<long[]> history = (List<long[]>) f.get(st);
+            return history != null ? history : Collections.emptyList();
         } catch (Exception e) {
             return Collections.emptyList();
         }
     }
 
     private static int getHistorySize(ScreenTerminal st) {
-        try {
-            Method m = ScreenTerminal.class.getMethod("getHistorySize");
-            return (int) m.invoke(st);
-        } catch (Exception e) {
-            return 0;
-        }
+        return getHistory(st).size();
     }
 
     private static class DelegateOutputStream extends OutputStream {


Reply via email to