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

davsclaus pushed a commit to branch CAMEL-26721-chaos-describe-input
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 4316bdaf16e04e444d7bb3806ab258b0c3d827a0
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 7 21:08:23 2026 +0100

    CAMEL-26721: camel-jbang - describe the chaos you want in your own words, 
and the AI turns it into checked rules
    
    The last part of S2-5 in the stage 2 order decided on CAMEL-26607.
    
    On the chaos screen, d (describe with AI, when the AI panel is there) opens
    an input for the chaos the user wants, such as "the order route should
    fail 5% of the time, or when the country is spain". Enter sends it to the
    AI panel at once (MonitorContext.AskAi.askNow, AiPanel.askNow; when the
    panel is busy it waits in the input) with the routes and sends of the
    integration, its rules now, the rule syntax in short, and the steps: check
    the rules, show each in plain words with what it hits, and add them.
    
    The AI panel had no chaos tools (CAMEL-26608), which a live try with a
    local model showed: it looked around the TUI for 76s and 18 tool calls.
    So the chaos tool group of the panel now has tui_chaos_check (read-only),
    tui_chaos_add and tui_chaos_change, while chaos is on; they run the
    shared chaos_check, chaos_add and chaos_change, and add and change ask
    the user first (ToolConfirms.chaos, which now names the integration and
    lists several rules one per line). Like the semantic tools they are left
    out of the full tool set unless their group is loaded, so the prompt of
    an integration without chaos does not grow. The same try then took 13s
    and one tool call.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/images/jbang/camel-tui-chaos-describe.png | Bin 0 -> 514742 bytes
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   8 ++
 .../dsl/jbang/core/commands/ai/ToolConfirms.java   |  43 +++++++++-
 .../jbang/core/commands/ai/ToolConfirmsTest.java   |   6 ++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  11 +++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   8 ++
 .../dsl/jbang/core/commands/tui/ChaosAiPrompt.java |  84 +++++++++++++++++++
 .../dsl/jbang/core/commands/tui/ChaosControl.java  |  44 ++++++++--
 .../jbang/core/commands/tui/MonitorContext.java    |   9 ++
 .../core/commands/tui/TuiToolDefinitions.java      |  28 +++++++
 .../dsl/jbang/core/commands/tui/TuiToolGroups.java |  17 +++-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  46 ++++++++++-
 .../src/main/resources/tui/help/chaos.md           |   9 ++
 .../core/commands/tui/AiPanelToolGroupsTest.java   |  11 ++-
 .../jbang/core/commands/tui/ChaosAiPromptTest.java |  91 +++++++++++++++++++++
 .../core/commands/tui/McpFacadeConfirmTest.java    |  26 ++++++
 .../jbang/core/commands/tui/SourceSizeTest.java    |   2 +-
 17 files changed, 426 insertions(+), 17 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-describe.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-describe.png
new file mode 100644
index 000000000000..9e7c66382c0b
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-describe.png differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index cc4a7d617e96..cf5a29cdaca8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -285,6 +285,14 @@ the route, and of its polls when it polls.
 image::jbang/camel-tui-chaos-here.png[! on the to callStock of the Diagram: 
the known rules for its HTTP endpoint and its step, with the endpoint filled in]
 * *x* removes the selected rule, *c* removes all rules (the report keeps what 
they did), and *R* starts a new experiment:
 no rules and a new report, with the same seed.
+* *d* describes the chaos with AI, when the AI panel is there: say the chaos 
you want in your own words, such as "the
+order route fails 5% of the time, or when the country is Spain". Enter sends 
it to the AI panel with the routes, the
+sends and the rules of the integration. The AI writes the rules, checks them 
with `tui_chaos_check` (each in plain
+words, and what it would hit), shows them, and adds them with `tui_chaos_add` 
(or changes a rule with
+`tui_chaos_change`, which keeps its id and gives it a new version), asking you 
to confirm each one. It does not change
+the routes. The AI panel has these tools only while chaos is on in the 
integration.
++
+image::jbang/camel-tui-chaos-describe.png[Describe with AI: the AI checked the 
two rules of "the order route should fail 5% of the time, or when the country 
is spain", and asks to add them]
 
 Each asks to confirm with *y*, with the rule shown. A rule that is not valid 
comes back with the reason, and a rule that
 matches nothing in the integration (often a typo) with a warning; press *e* 
there to change it.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
index 855e184a08fe..54d15487ed5b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
@@ -303,14 +303,55 @@ public final class ToolConfirms {
         String where = integration != null && !integration.isBlank() ? 
integration : "the selected integration";
         String reason = "While the rule is active, the integration's messages 
fail, wait or get lost; exception= can name"
                         + " any exception class";
+        List<String> rules = c.isEmpty() ? List.of() : splitRules(rule);
         return switch (c) {
-            case "add" -> new Call("add a chaos rule to " + where, rule, 
reason);
+            // several rules separated by ; are added together: one per line, 
so each can be read
+            case "add" -> rules.size() > 1
+                    ? new Call("add " + rules.size() + " chaos rules to " + 
where, String.join("\n", rules), reason)
+                    : new Call("add a chaos rule to " + where, rule, reason);
             case "change" -> id != null && !id.isBlank()
                     ? new Call("change chaos rule " + id + " of " + where, 
rule, reason) : null;
             default -> null;
         };
     }
 
+    /**
+     * The rules of a text with several separated by {@code ;}, as 
camel.chaos.rules: a {@code ;} in quotes does not.
+     */
+    static List<String> splitRules(String text) {
+        List<String> answer = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        char quote = 0;
+        for (int i = 0; i < text.length(); i++) {
+            char ch = text.charAt(i);
+            if (quote != 0) {
+                current.append(ch);
+                if (ch == '\\' && i + 1 < text.length()) {
+                    current.append(text.charAt(++i));
+                } else if (ch == quote) {
+                    quote = 0;
+                }
+            } else if (ch == ';') {
+                addRule(answer, current);
+            } else {
+                if (ch == '"' || ch == '\'') {
+                    quote = ch;
+                }
+                current.append(ch);
+            }
+        }
+        addRule(answer, current);
+        return answer;
+    }
+
+    private static void addRule(List<String> rules, StringBuilder text) {
+        String rule = text.toString().strip();
+        if (!rule.isEmpty()) {
+            rules.add(rule);
+        }
+        text.setLength(0);
+    }
+
     /** Running an integration on this machine. */
     public static Call run(String what) {
         return new Call("run an integration", what, "It starts an integration 
on this machine with the project's code");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
index 63e11ca544ca..f868d9b950e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
@@ -292,6 +292,12 @@ class ToolConfirmsTest {
         assertThat(add.target()).isEqualTo("endpoint=* error 
exception=java.sql.SQLException");
         assertThat(add.reason()).contains("fail, wait or get lost", 
"exception=");
 
+        // several rules are added together: one per line, and a ; inside 
quotes does not separate
+        ToolConfirms.Call two = ToolConfirms.chaos("add", "chaosdemo", null,
+                "route=orders error rate=0.05; route=orders error 
when=\"${header.note} == 'a;b'\"");
+        assertThat(two.title()).isEqualTo("add 2 chaos rules to chaosdemo");
+        assertThat(two.target()).isEqualTo("route=orders error 
rate=0.05\nroute=orders error when=\"${header.note} == 'a;b'\"");
+
         ToolConfirms.Call change = ToolConfirms.chaos("change", null, "r1", 
"endpoint=* drop");
         assertThat(change.title()).isEqualTo("change chaos rule r1 of the 
selected integration");
         assertThat(change.target()).isEqualTo("endpoint=* drop");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 036db0262b07..f3f75f40c949 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -598,6 +598,17 @@ class AiPanel {
         inputLine.replace(question);
     }
 
+    /**
+     * Opens the panel and sends the question, which the user typed and 
confirmed on a screen. While the panel answers
+     * another question, or asks about an edit, the question waits in the 
input for the user to send.
+     */
+    void askNow(String question) {
+        openWithQuestion(question);
+        if (!thinking.get() && editQuestionHandler == null && activeCliCommand 
== null) {
+            submitInput();
+        }
+    }
+
     boolean isAskingAboutEdit() {
         return editQuestionHandler != null;
     }
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 a26335cc6e09..bb16c25468fa 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
@@ -601,6 +601,14 @@ public class CamelMonitor extends CamelCommand {
             public void ask(String question) {
                 openAiWithQuestion(dir -> question);
             }
+
+            @Override
+            public void askNow(String question) {
+                if (shellPanel.isOpen()) {
+                    shellPanel.close();
+                }
+                aiPanel.askNow(question);
+            }
         };
         ctx.projectOverviewCallback = () -> {
             if (shellPanel.isOpen()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
index 552ee7baeaaf..3898bbf4234b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
@@ -48,6 +48,90 @@ final class ChaosAiPrompt {
         return sb.toString();
     }
 
+    /** The most routes and sends quoted in a request, so a big integration 
does not flood it. */
+    static final int MAX_LISTED = 20;
+
+    /**
+     * The question for a request in the user's own words ({@code d} on the 
Chaos screen, CAMEL-26721), such as "the
+     * order route should fail 5% of the time": the request, the routes and 
the sends of the integration and its rules
+     * now, the rule syntax in short, and what to do - write the rules, check 
them with tui_chaos_check (which says what
+     * each would hit, in plain words), show them, and add them only with 
tui_chaos_add (or change one with
+     * tui_chaos_change), which ask the user first.
+     */
+    static String ofRequest(IntegrationInfo info, String request) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Turn my request into camel-chaos rules for the running 
integration ").append(name(info))
+                .append(" (development only, faults injected on purpose). My 
request: ").append(request.strip())
+                .append('\n');
+        int listed = 0;
+        StringBuilder routes = new StringBuilder();
+        for (RouteInfo route : info.routes) {
+            if (route.routeId != null && listed++ < MAX_LISTED) {
+                routes.append("  ").append(route.routeId);
+                if (route.from != null) {
+                    routes.append(" from ").append(route.from);
+                }
+                routes.append('\n');
+            }
+        }
+        if (!routes.isEmpty()) {
+            sb.append("Its routes:\n").append(routes);
+        }
+        // the endpoint a route consumes is not one of its sends (a send to it 
from another route is)
+        List<String> consumed = new ArrayList<>();
+        info.routes.forEach(r -> {
+            if (r.from != null && r.routeId != null) {
+                consumed.add(r.routeId + " " + sameUri(r.from));
+            }
+        });
+        List<String> sends = new ArrayList<>();
+        for (EndpointInfo endpoint : info.endpoints) {
+            if ("out".equals(endpoint.direction) && endpoint.uri != null && 
sends.size() < MAX_LISTED
+                    && !consumed.contains(endpoint.routeId + " " + 
sameUri(endpoint.uri))) {
+                String send = withoutQuery(endpoint.uri) + (endpoint.routeId 
!= null ? " (from " + endpoint.routeId + ")" : "");
+                if (!sends.contains(send)) {
+                    sends.add(send);
+                }
+            }
+        }
+        if (!sends.isEmpty()) {
+            sb.append("It sends to:\n");
+            sends.forEach(s -> sb.append("  ").append(s).append('\n'));
+        }
+        ChaosInfo chaos = info.chaos;
+        if (chaos != null && !chaos.rules.isEmpty()) {
+            sb.append("Its chaos rules now:\n");
+            chaos.rules.forEach(r -> sb.append("  ").append(r.id).append(": 
").append(r.text).append('\n'));
+        }
+        sb.append("A rule is one line: <target> <fault> [<parameter>=<value> 
...] [<option>=<value> ...]. Targets:")
+                .append(" endpoint=<uri pattern>, component=<scheme>, 
route=<route id>, step=<step id>, with * for any")
+                .append(" characters. Faults: error (exception=<class>, 
message=, status=<http code> for http),")
+                .append(" delay (duration=500ms or 100ms..2s), drop, corrupt 
(mode=headers|charset|truncate|empty),")
+                .append(" duplicate, stall (route=, duration=). Options: 
rate=0.05 (or every:N), until=10m,")
+                .append(" in=<route id> for the sends and steps of a route, 
when=\"${header.country} == 'ES'\" for")
+                .append(" the messages that meet a condition (simple 
language). route=<id> error fails 5% of the")
+                .append(" exchanges of a route with rate=0.05; \"or\" is two 
rules. Each rule has its own rate: without")
+                .append(" rate= a rule fails every message it matches.\n");
+        sb.append("The routes and sends above are all you need: do not look 
around with other tools. First call ")
+                .append(TuiToolGroups.CHAOS_CHECK_TOOL).append(" with the 
rules (several separated by ;): it says each")
+                .append(" rule in plain words and what it would hit, or why it 
is not valid. Fix the rules until it")
+                .append(" accepts them and they hit what I asked for. Then 
show me each rule with its plain words and")
+                .append(" what it hits, and add them with 
").append(TuiToolGroups.CHAOS_ADD_TOOL)
+                .append(" (or change a rule above with 
").append(TuiToolGroups.CHAOS_CHANGE_TOOL)
+                .append("), which asks me to confirm first. Do not change the 
routes.");
+        return sb.toString();
+    }
+
+    // a URI without its query and the // after its scheme, to tell the 
endpoint a route consumes
+    private static String sameUri(String uri) {
+        return withoutQuery(uri).replaceFirst(":/+", ":");
+    }
+
+    private static String withoutQuery(String uri) {
+        int query = uri.indexOf('?');
+        return query > 0 ? uri.substring(0, query) : uri;
+    }
+
     /** The question for an error a chaos rule made, on the Errors tab: it is 
not a bug to fix. */
     static String ofError(IntegrationInfo info, ChaosInfo.Rule rule, String 
ruleId, ErrorInfo error) {
         StringBuilder sb = new StringBuilder();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
index 502211cb0e5b..6fb33bb27505 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
@@ -65,7 +65,10 @@ final class ChaosControl {
         CHANGE,
         REMOVE,
         CLEAR,
-        RESET
+        RESET,
+        // the chaos the user wants, in their own words, for the AI panel to 
turn into rules (not sent to the
+        // integration)
+        DESCRIBE
     }
 
     private final MonitorContext ctx;
@@ -87,6 +90,8 @@ final class ChaosControl {
     private String resultTitle;
     private final List<String> resultLines = new ArrayList<>();
     private boolean resultError;
+    // the integration the user describes the chaos for
+    private IntegrationInfo describing;
 
     ChaosControl(MonitorContext ctx) {
         this(ctx, null, ChaosRecipes::load);
@@ -200,6 +205,12 @@ final class ChaosControl {
             ask(Command.RESET);
             return true;
         }
+        if (ke.isChar('d') && ctx.askAiCallback != null) {
+            // the chaos the user wants in their own words, which the AI turns 
into rules, checks and adds (CAMEL-26721)
+            describing = info;
+            openInput(Command.DESCRIBE, "");
+            return true;
+        }
         return false;
     }
 
@@ -260,7 +271,13 @@ final class ChaosControl {
             close();
         } else if (ke.isConfirm()) {
             String text = input.text().strip();
-            if (!text.isEmpty()) {
+            if (!text.isEmpty() && command == Command.DESCRIBE) {
+                // the AI panel asks right away: the user typed the request 
and pressed Enter; the rules it makes are
+                // added only through the add tool, which asks the user first
+                IntegrationInfo info = describing;
+                close();
+                ctx.askAiCallback.askNow(ChaosAiPrompt.ofRequest(info, text));
+            } else if (!text.isEmpty()) {
                 rule = text;
                 ask(command);
             }
@@ -310,6 +327,7 @@ final class ChaosControl {
         step = Step.NONE;
         input = null;
         form = null;
+        describing = null;
     }
 
     /** The request of the chaos action for the command, as {@code camel cmd 
chaos-*} sends it. */
@@ -376,6 +394,8 @@ final class ChaosControl {
             case REMOVE -> "Removed chaos rule " + id;
             case CLEAR -> "Removed all chaos rules";
             case RESET -> "New chaos experiment: no rules, a new report";
+            // never sent to the integration
+            case DESCRIBE -> "";
         };
         if (reply.get("warnings") instanceof List<?> warnings && 
!warnings.isEmpty()) {
             List<String> lines = new ArrayList<>();
@@ -424,9 +444,16 @@ final class ChaosControl {
     void render(Frame frame, Rect area) {
         switch (step) {
             case RECIPES -> form.render(frame, area);
-            case INPUT -> DialogHelper.renderInputDialog(frame, area,
-                    command == Command.CHANGE ? "Change chaos rule " + id : 
"Add a chaos rule to " + name, input,
-                    "endpoint=kafka:orders* error rate=0.1 until=5m");
+            case INPUT -> {
+                if (command == Command.DESCRIBE) {
+                    DialogHelper.renderInputDialog(frame, area, "Describe the 
chaos you want in " + name, input,
+                            "the order route fails 5% of the time, or when the 
country is Spain");
+                } else {
+                    DialogHelper.renderInputDialog(frame, area,
+                            command == Command.CHANGE ? "Change chaos rule " + 
id : "Add a chaos rule to " + name, input,
+                            "endpoint=kafka:orders* error rate=0.1 until=5m");
+                }
+            }
             case CONFIRM -> DialogHelper.renderConfirm(frame, area, title(), 
question(), detail(), "y", "yes", null, null);
             case RESULT -> renderResult(frame, area);
             default -> {
@@ -442,6 +469,7 @@ final class ChaosControl {
             case REMOVE -> "Remove Chaos Rule";
             case CLEAR -> "Clear Chaos Rules";
             case RESET -> "New Chaos Experiment";
+            case DESCRIBE -> "Describe the Chaos";
         };
     }
 
@@ -453,6 +481,7 @@ final class ChaosControl {
             case REMOVE -> "Remove chaos rule " + id + " from " + name + "?";
             case CLEAR -> "Remove all chaos rules from " + name + "? The 
report keeps what they did";
             case RESET -> "Start a new chaos experiment in " + name + "? No 
rules, and a new report (same seed)";
+            case DESCRIBE -> "Ask the AI to turn this into chaos rules?";
         };
     }
 
@@ -501,7 +530,7 @@ final class ChaosControl {
         switch (step) {
             case RECIPES -> form.renderFooter(spans);
             case INPUT -> {
-                TuiHelper.hint(spans, "Enter", "confirm");
+                TuiHelper.hint(spans, "Enter", command == Command.DESCRIBE ? 
"ask the AI" : "confirm");
                 TuiHelper.hint(spans, "Esc", "cancel");
             }
             case CONFIRM -> {
@@ -518,6 +547,9 @@ final class ChaosControl {
                     TuiHelper.hint(spans, "c", "clear");
                 }
                 TuiHelper.hint(spans, "R", "new experiment");
+                if (ctx.askAiCallback != null) {
+                    TuiHelper.hint(spans, "d", "describe with AI");
+                }
             }
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index b207871f1fd7..74ae874f420b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -403,5 +403,14 @@ class MonitorContext {
          */
         default void ask(String question) {
         }
+
+        /**
+         * Opens the AI panel and sends the question at once, for a request 
the user typed and confirmed on a screen
+         * ({@code d} on the Chaos screen). When the panel is busy, the 
question waits in its input, as with
+         * {@link #ask(String)}.
+         */
+        default void askNow(String question) {
+            ask(question);
+        }
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index 2e06525cf032..129b830242c2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -50,6 +50,7 @@ final class TuiToolDefinitions {
         List<TuiTool> tools = new ArrayList<>();
         addInteractionTools(tools, r);
         addStructuredDataTools(tools, r);
+        addChaosTools(tools, r);
         addDiagramTools(tools, r);
         addSharedTools(tools, r);
         addCatalogTools(tools, r);
@@ -294,6 +295,33 @@ final class TuiToolDefinitions {
                 r::callDiagramZoom));
     }
 
+    /**
+     * The chaos rules of the selected integration (camel-chaos), which only 
its tool group adds when chaos is on: check
+     * a rule, and add or change one after the user confirms (CAMEL-26721).
+     */
+    private static void addChaosTools(List<TuiTool> tools, TuiToolRegistry r) {
+        tools.add(tool(toolDef(
+                TuiToolGroups.CHAOS_CHECK_TOOL,
+                "Checks chaos rules without adding them: each rule in plain 
words, where it can fire, warnings, and "
+                                                + "what it would hit now 
(hits: sends, polls, routes or steps with "
+                                                + "their route); a rule that 
is not valid comes back with the reason.",
+                Map.of("rule", propDef("string", "One-line rule, or several 
separated by ;")),
+                List.of("rule")), r::callChaosCheck, READ_ONLY));
+        tools.add(tool(toolDef(
+                TuiToolGroups.CHAOS_ADD_TOOL,
+                "Adds chaos rules to the selected integration after the user 
confirms; returns their ids and plain "
+                                              + "words. Check them with " + 
TuiToolGroups.CHAOS_CHECK_TOOL + " first.",
+                Map.of("rule", propDef("string", "One-line rule, or several 
separated by ;")),
+                List.of("rule")), r::callChaosAdd));
+        tools.add(tool(toolDef(
+                TuiToolGroups.CHAOS_CHANGE_TOOL,
+                "Changes a chaos rule after the user confirms: the rule with 
the id gets the new rule and its next "
+                                                 + "version; the report keeps 
the old one.",
+                Map.of("id", propDef("string", "The id of the rule, such as 
r1"),
+                        "rule", propDef("string", "The new one-line rule")),
+                List.of("id", "rule")), r::callChaosChange));
+    }
+
     private static void addStructuredDataTools(List<TuiTool> tools, 
TuiToolRegistry r) {
         tools.add(tool(toolDef(
                 "tui_get_table",
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
index 070491a70878..523dbc905645 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
@@ -37,6 +37,9 @@ final class TuiToolGroups {
     static final String UPDATE_ROW_TOOL = "tui_update_row";
     static final String HTTP_ENDPOINTS_TOOL = "tui_http_endpoints";
     static final String HTTP_REQUEST_TOOL = "tui_http_request";
+    static final String CHAOS_CHECK_TOOL = "tui_chaos_check";
+    static final String CHAOS_ADD_TOOL = "tui_chaos_add";
+    static final String CHAOS_CHANGE_TOOL = "tui_chaos_change";
 
     /**
      * The groups of an integration as the panel uses them.
@@ -78,6 +81,11 @@ final class TuiToolGroups {
                 if (f.semanticMonitoring()) {
                     tools.add("tui_get_monitoring");
                 }
+            } else if (group == ToolGroup.CHAOS) {
+                // with chaos off the rules can only be refused
+                if (f.chaosOn()) {
+                    tools.addAll(tools(group));
+                }
             } else {
                 tools.addAll(tools(group));
             }
@@ -91,6 +99,7 @@ final class TuiToolGroups {
             case SQL -> List.of(SQL_TOOL, UPDATE_ROW_TOOL);
             case HTTP -> List.of(HTTP_ENDPOINTS_TOOL, HTTP_REQUEST_TOOL);
             case SEMANTIC -> List.of("tui_get_audit", "tui_get_monitoring");
+            case CHAOS -> List.of(CHAOS_CHECK_TOOL, CHAOS_ADD_TOOL, 
CHAOS_CHANGE_TOOL);
             default -> List.of();
         };
     }
@@ -119,11 +128,13 @@ final class TuiToolGroups {
                                + " rate; OPEN means the fallback runs.";
             case HTTP -> "HTTP: " + ToolGroups.describeHttp(f) + "; " + 
HTTP_REQUEST_TOOL + " calls it, "
                          + HTTP_ENDPOINTS_TOOL + " lists the operations.";
-            // the panel has no chaos tools (camel ask and camel-jbang-mcp 
have them): it only learns that faults may be
-            // injected on purpose, so it does not hunt for a bug behind them
+            // faults may be injected on purpose, so the panel does not hunt 
for a bug behind them; it checks, adds and
+            // changes rules as camel ask does, adding and changing after the 
user confirms (CAMEL-26721)
             case CHAOS -> f.chaosOn()
                     ? "Chaos: camel-chaos is on, so some failures and delays 
may be injected on purpose; tui_get_status"
-                      + " section 'chaos' has the rules, tui_get_table tab 
'Chaos' their live state and effect."
+                      + " section 'chaos' has the rules, tui_get_table tab 
'Chaos' their live state and effect; "
+                      + CHAOS_CHECK_TOOL + " checks rules, " + CHAOS_ADD_TOOL 
+ " and " + CHAOS_CHANGE_TOOL
+                      + " add and change them after the user confirms."
                     : "Chaos: camel-chaos is in the integration but off, so it 
injects no faults.";
         };
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 63d187fea689..1d6d3f929bd4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -259,10 +259,14 @@ class TuiToolRegistry {
         return tools;
     }
 
+    // the tools that only the tool groups of the integration add, also to the 
full set: the semantic history, and
+    // the chaos rules when camel-chaos is on
+    private static final Set<String> GROUP_ONLY_TOOLS = 
Set.of("tui_get_audit", "tui_get_monitoring",
+            TuiToolGroups.CHAOS_CHECK_TOOL, TuiToolGroups.CHAOS_ADD_TOOL, 
TuiToolGroups.CHAOS_CHANGE_TOOL);
+
     List<ToolDef> getAvailableToolDefinitions(Collection<String> selected) {
         return getToolDefinitions().stream()
-                .filter(tool -> !Set.of("tui_get_audit", 
"tui_get_monitoring").contains(tool.name())
-                        || selected.contains(tool.name()))
+                .filter(tool -> !GROUP_ONLY_TOOLS.contains(tool.name()) || 
selected.contains(tool.name()))
                 .toList();
     }
 
@@ -1341,6 +1345,44 @@ class TuiToolRegistry {
         return Jsoner.serialize(response);
     }
 
+    /** Checks chaos rules in the selected integration (chaos_check), which 
adds nothing. */
+    String callChaosCheck(Map<String, Object> args) {
+        String rule = args.get("rule") instanceof String s ? s : null;
+        if (rule == null || rule.isBlank()) {
+            return "Error: rule is required";
+        }
+        return executeShared("chaos_check", Map.of("rule", rule));
+    }
+
+    /** Adds chaos rules to the selected integration (chaos_add), after the 
user confirms. */
+    String callChaosAdd(Map<String, Object> args) {
+        String rule = args.get("rule") instanceof String s ? s : null;
+        if (rule == null || rule.isBlank()) {
+            return "Error: rule is required";
+        }
+        JsonObject rejected = facade != null
+                ? facade.confirm(ToolConfirms.chaos("add", 
facade.getSelectedIntegrationName(), null, rule)) : null;
+        if (rejected != null) {
+            return Jsoner.serialize(rejected);
+        }
+        return executeShared("chaos_add", Map.of("rule", rule));
+    }
+
+    /** Changes a chaos rule of the selected integration (chaos_change), after 
the user confirms. */
+    String callChaosChange(Map<String, Object> args) {
+        String id = args.get("id") instanceof String s ? s : null;
+        String rule = args.get("rule") instanceof String s ? s : null;
+        if (id == null || id.isBlank() || rule == null || rule.isBlank()) {
+            return "Error: id and rule are required";
+        }
+        JsonObject rejected = facade != null
+                ? facade.confirm(ToolConfirms.chaos("change", 
facade.getSelectedIntegrationName(), id, rule)) : null;
+        if (rejected != null) {
+            return Jsoner.serialize(rejected);
+        }
+        return executeShared("chaos_change", Map.of("id", id, "rule", rule));
+    }
+
     String callUpdateRow(Map<String, Object> args) {
         String table = (String) args.get("table");
         if (table == null || table.isBlank()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
index c321456cf809..29df37a7f5ae 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
@@ -77,6 +77,15 @@ error handler (HAND) and the redeliveries (REDEL).
   says what the fault simulates and how the routes coped, and suggests how
   to make them cope better, changing the routes only when you agree.
   Press Enter to send the question, or change it first
+- **d** — Describe with AI: say the chaos you want in your own words, such
+  as "the order route fails 5% of the time, or when the country is Spain".
+  Enter sends it to the AI panel with the routes, the sends and the rules
+  of the integration. The AI writes the rules, checks them with
+  `tui_chaos_check` (each rule in plain words, and what it would hit),
+  shows them to you, and adds them with `tui_chaos_add` (or changes a rule
+  with `tui_chaos_change`, which keeps its id and gives it a new version),
+  asking you to confirm each one. It does not change the routes. The AI
+  panel has these tools only while chaos is on in the integration
 
 Each asks to confirm with **y**, showing the rule. The integration checks
 the rule as `camel.chaos.rules` is checked: a rule that is not valid comes
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
index 666fe4879c02..663c0165c508 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
@@ -236,19 +236,22 @@ class AiPanelToolGroupsTest {
     }
 
     @Test
-    void theChaosGroupAddsOnlyALine() {
-        // the panel has no chaos tools; its model learns that faults may be 
injected on purpose
+    void theChaosGroupAddsTheRuleToolsWhenChaosIsOn() {
+        // its model learns that faults may be injected on purpose, and 
checks, adds and changes rules (CAMEL-26721)
         TuiToolGroups.Selection on = TuiToolGroups.select(
                 AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", true)))));
         assertEquals(List.of(ToolGroup.CHAOS), on.groups());
-        assertTrue(on.tools().isEmpty(), on.tools().toString());
+        assertEquals(List.of("tui_chaos_check", "tui_chaos_add", 
"tui_chaos_change"), on.tools());
         assertEquals(List.of("Chaos: camel-chaos is on, so some failures and 
delays may be injected on purpose;"
                              + " tui_get_status section 'chaos' has the rules, 
tui_get_table tab 'Chaos' their live"
-                             + " state and effect."),
+                             + " state and effect; tui_chaos_check checks 
rules, tui_chaos_add and tui_chaos_change add"
+                             + " and change them after the user confirms."),
                 on.guidance());
 
+        // with chaos off a rule can only be refused: no tools
         TuiToolGroups.Selection off = TuiToolGroups.select(
                 AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", false)))));
+        assertTrue(off.tools().isEmpty(), off.tools().toString());
         assertEquals(List.of("Chaos: camel-chaos is in the integration but 
off, so it injects no faults."),
                 off.guidance());
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
index b3f5a7f5c355..de969a3821b1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
@@ -78,9 +78,100 @@ class ChaosAiPromptTest {
             public void ask(String question) {
                 asked.add(question);
             }
+
+            @Override
+            public void askNow(String question) {
+                asked.add("now: " + question);
+            }
         };
     }
 
+    @Test
+    void aRequestInTheUsersWordsWithTheRoutesTheSendsAndTheRules() {
+        RouteInfo orders = new RouteInfo();
+        orders.routeId = "orders";
+        orders.from = "timer://orders?period=500";
+        info.routes.add(orders);
+        EndpointInfo stock = new EndpointInfo();
+        stock.uri = "http://localhost:8765/stock?bridgeEndpoint=true";;
+        stock.direction = "out";
+        stock.routeId = "orders";
+        info.endpoints.add(stock);
+        EndpointInfo in = new EndpointInfo();
+        in.uri = "timer://orders";
+        in.direction = "in";
+        info.endpoints.add(in);
+        // a route that serves HTTP: its own endpoint is not a send, a send to 
it from orders is
+        RouteInfo stockRoute = new RouteInfo();
+        stockRoute.routeId = "stock";
+        stockRoute.from = "platform-http:/stock";
+        info.routes.add(stockRoute);
+        EndpointInfo served = new EndpointInfo();
+        served.uri = "platform-http:///stock";;
+        served.direction = "out";
+        served.routeId = "stock";
+        info.endpoints.add(served);
+        EndpointInfo direct = new EndpointInfo();
+        direct.uri = "direct://inventory";
+        direct.direction = "out";
+        direct.routeId = "orders";
+        info.endpoints.add(direct);
+
+        String question = ChaosAiPrompt.ofRequest(info, " the order route 
fails 5% of the time, or for Spain ");
+
+        assertThat(question).startsWith("Turn my request into camel-chaos 
rules for the running integration chaosdemo"
+                                        + " (development only, faults injected 
on purpose). My request: the order route"
+                                        + " fails 5% of the time, or for 
Spain\n")
+                .contains("Its routes:\n  orders from 
timer://orders?period=500\n")
+                // the sends, without their query; not the consumers
+                .contains("It sends to:\n  http://localhost:8765/stock (from 
orders)\n  direct://inventory (from orders)\n")
+                .doesNotContain("platform-http:///stock (from stock)")
+                .doesNotContain("bridgeEndpoint").doesNotContain("  
timer://orders (from")
+                .contains("Its chaos rules now:\n  r1: 
endpoint=http://localhost:8765/stock* error status=503")
+                .contains("in=<route id>").contains("when=\"${header.country} 
== 'ES'\"").contains("\"or\" is two rules")
+                .contains("Each rule has its own rate: without rate= a rule 
fails every message it matches.")
+                .contains("do not look around with other 
tools").contains("First call tui_chaos_check with the rules")
+                .endsWith("add them with tui_chaos_add (or change a rule above 
with tui_chaos_change), which asks me to"
+                          + " confirm first. Do not change the routes.");
+    }
+
+    @Test
+    void dOnTheChaosScreenAsksTheAiWithTheRequest() {
+        ChaosTab tab = new ChaosTab(ctx);
+        TuiTestHelper.renderToString(tab, 160, 40);
+        List<Span> footer = new ArrayList<>();
+        tab.renderFooter(footer);
+        assertThat(text(footer)).contains("describe with AI");
+
+        tab.handleKeyEvent(KeyEvent.ofChar('d', KeyModifiers.NONE));
+        assertThat(tab.isTextInputActive()).isTrue();
+        assertThat(TuiTestHelper.renderToString(tab, 160, 
40)).contains("Describe the chaos you want in chaosdemo");
+        // Enter on an empty request does nothing
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        assertThat(asked).isEmpty();
+        for (char ch : "shipping is slow".toCharArray()) {
+            tab.handleKeyEvent(KeyEvent.ofChar(ch, KeyModifiers.NONE));
+        }
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        // asked at once, as the user typed it and pressed Enter; nothing goes 
to the integration
+        assertThat(asked).singleElement().asString().startsWith("now: Turn my 
request into camel-chaos rules")
+                .contains("My request: shipping is slow");
+        assertThat(tab.control().isOpen()).isFalse();
+    }
+
+    @Test
+    void withoutTheAiThereIsNoDescribe() {
+        ctx.askAiCallback = null;
+        ChaosTab tab = new ChaosTab(ctx);
+        TuiTestHelper.renderToString(tab, 160, 40);
+        tab.handleKeyEvent(KeyEvent.ofChar('d', KeyModifiers.NONE));
+        assertThat(tab.control().isOpen()).isFalse();
+        List<Span> footer = new ArrayList<>();
+        tab.renderFooter(footer);
+        assertThat(text(footer)).doesNotContain("describe");
+    }
+
     @Test
     void theQuestionHasTheRuleItsEffectAndTheRoutesItHit() {
         String question = ChaosAiPrompt.of(info, info.chaos.rule("r1"));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
index 9a3c5573681d..2c7630f4faf5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeConfirmTest.java
@@ -117,6 +117,32 @@ class McpFacadeConfirmTest {
                 .containsExactly("run SQL that is not a SELECT", "update a row 
of orders");
     }
 
+    @Test
+    void addingAndChangingAChaosRuleAskFirstAndCheckingDoesNot() throws 
Exception {
+        AnsweringBridge bridge = new AnsweringBridge(false);
+        TuiToolRegistry registry = new TuiToolRegistry(facade(context(), 
bridge));
+
+        JsonObject added = (JsonObject) 
Jsoner.deserialize(registry.execute("tui_chaos_add",
+                new JsonObject(Map.of("rule", "route=orders error 
rate=0.05"))));
+        JsonObject changed = (JsonObject) 
Jsoner.deserialize(registry.execute("tui_chaos_change",
+                new JsonObject(Map.of("id", "r1", "rule", "route=orders error 
rate=0.1"))));
+
+        // not confirmed: nothing went to the integration
+        assertThat(added.getString("status")).isEqualTo("rejected");
+        assertThat(changed.getString("status")).isEqualTo("rejected");
+        assertThat(bridge.calls).extracting(ToolConfirms.Call::target)
+                .containsExactly("route=orders error rate=0.05", "route=orders 
error rate=0.1");
+        assertThat(registry.execute("tui_chaos_add", new 
JsonObject(Map.of()))).isEqualTo("Error: rule is required");
+
+        // the check only reads: it is not asked (here no integration answers 
it)
+        try {
+            registry.execute("tui_chaos_check", new JsonObject(Map.of("rule", 
"route=orders error")));
+        } catch (RuntimeException e) {
+            // no integration with pid 1 runs in the test
+        }
+        assertThat(bridge.calls).hasSize(2);
+    }
+
     @Test
     void groovyAsksFirstThroughTheTool() throws Exception {
         AnsweringBridge bridge = new AnsweringBridge(false);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index 0b0b7534ec34..fe7d84bb4d54 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -59,7 +59,7 @@ class SourceSizeTest {
             AiPanel.java 3200
             AiPanel.runAgentLoop 159
             AiUsageStats.render 166
-            CamelMonitor.java 2624
+            CamelMonitor.java 2632
             DiagramSupport.java 2360
             DiagramSupport.loadHighlightedNativeDiagramInBackground 200
             EndpointsTab.renderContent 170

Reply via email to