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 7514fbc164da CAMEL-25358: camel-jbang - source editor: Shift+F8 (fix 
with AI) on a line that fails at runtime
7514fbc164da is described below

commit 7514fbc164dac65dd5c9e2062a39dbadda52c903
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 07:16:12 2026 +0200

    CAMEL-25358: camel-jbang - source editor: Shift+F8 (fix with AI) on a line 
that fails at runtime
    
    Shift+F8 in the Source editor and view now also works on a line whose
    processors fail at runtime (the failures in the live run data). The AI panel
    opens with how many exchanges failed on the line and the exception of the 
last
    one, read from the integration's errors. It asks the AI to find the cause 
first,
    since it may lie elsewhere (e.g. a direct: endpoint no route consumes). A
    problem on the line still goes first, and a file with unsaved edits has no
    runtime failures. Lines that only count a failure (from, choice) get the 
last
    error in the file with its line.
    
    Shift+F8 in the Errors tab asks the AI to fix the selected error, with the
    source line of the processor it happened at.
    
    The Shift+F8 hint now sits with the other F-keys in the footer, and an
    overflowing footer drops the global F10/F2 before a screen's own F-key hints
    (F1 help always stays). Adds RuntimeFailures, AiFixPrompt.ofFailure and
    AskAi.fixFailure (default method), with FixRuntimeFailureTest and docs.
    
    Closes #27386
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../images/jbang/camel-tui-errors-fix-with-ai.png  | Bin 0 -> 345544 bytes
 .../jbang/camel-tui-source-fix-runtime-failure.png | Bin 0 -> 333911 bytes
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   6 +
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  12 +-
 .../dsl/jbang/core/commands/tui/AiFixPrompt.java   |  28 ++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  56 +++++-
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     |  49 +++++
 .../dsl/jbang/core/commands/tui/LiveRunLines.java  |  31 +--
 .../jbang/core/commands/tui/MonitorContext.java    |  14 +-
 .../jbang/core/commands/tui/RuntimeFailures.java   | 168 ++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  10 +
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 110 ++++++++++-
 .../jbang/core/commands/tui/CamelMonitorTest.java  |  25 +++
 .../core/commands/tui/FixRuntimeFailureTest.java   | 217 +++++++++++++++++++++
 14 files changed, 695 insertions(+), 31 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-errors-fix-with-ai.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-errors-fix-with-ai.png
new file mode 100644
index 000000000000..e0e7a42ef70f
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-errors-fix-with-ai.png 
differ
diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-fix-runtime-failure.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-fix-runtime-failure.png
new file mode 100644
index 000000000000..f586b719f814
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-fix-runtime-failure.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 78c044db9398..5ef6afb403bd 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
@@ -130,6 +130,12 @@ Inspect tab):
 This combination of error details + visual diagram replay gives you a complete 
picture of
 what went wrong and why -- without leaving the terminal.
 
+Press *Shift+F8* on an error to ask the AI to fix it: the AI panel opens with 
the source line of the processor the
+error happened at, and its exception, the same question as *Shift+F8* on a 
failing line in the
+xref:camel-jbang-tui-source-editor.adoc#_fix_with_ai[Source editor].
+
+image::jbang/camel-tui-errors-fix-with-ai.png[Shift+F8 on an error writes the 
question for the AI]
+
 image::jbang/camel-tui-observe-error-diagram.png[A failed order: the path from 
checkout to payment-provider, and the onException handler that parked it]
 
 == OpenTelemetry Spans
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 8fc4b368a97c..c4787850c838 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -12,7 +12,7 @@ routes.
 * <<_problems_as_you_type,*Problems as you type*>> -- endpoint options, Simple 
expressions and more are checked as soon
   as a file opens and while you type, in YAML, Java and XML routes; a YAML or 
XML file with problems is not saved.
 * <<_quick_fixes,*Quick fixes*>> -- *Shift+F9* applies the fix a problem 
names, such as an option typo.
-* <<_fix_with_ai,*Fix with AI*>> -- *Shift+F8* asks the AI to fix the problem 
of a line; you confirm every change.
+* <<_fix_with_ai,*Fix with AI*>> -- *Shift+F8* asks the AI to fix the problem 
of a line, or a line that fails at runtime; you confirm every change.
 * <<_completion,*Completion*>> -- *Tab* completes components, endpoint options 
and their values, Java DSL chains, XML
   elements and attributes, and simple expressions; light help for hand-written 
edits, see
   <<_known_limitations,Known limitations>>.
@@ -100,6 +100,14 @@ first.
 
 image::jbang/camel-tui-source-fix-with-ai.png[Shift+F8 writes the question for 
the AI]
 
+*Shift+F8* also works on a line that fails at runtime. While the integration 
runs, the live run data at the end of the
+lines counts the failed exchanges (✗); on such a line, *Shift+F8* asks the AI 
with how many exchanges failed and the
+exception of the last one, from the integration's errors. The cause is not 
always on that line (a `direct:` endpoint
+that no route consumes, for example), so the AI is asked to find it first. 
This works in the view as well as in the
+editor, as long as the file has no unsaved edits, and a problem on the line 
goes first.
+
+image::jbang/camel-tui-source-fix-runtime-failure.png[Shift+F8 on a line that 
fails at runtime writes the question for the AI]
+
 The AI changes the file with the same tools an AI agent uses over MCP, and 
every change waits for you: the dialog shows
 what the AI wants to change, *d* shows the diff, *Enter* applies it and *Esc* 
rejects it.
 
@@ -317,7 +325,7 @@ image::jbang/camel-tui-source-convert.png[An XML route 
converted to YAML, with t
 | *Ctrl+R* | Refactoring menu: replace URI, extract to property, extract a 
step to a new file (YAML, XML)
 | *F9* | Next problem
 | *Shift+F9* | Apply the quick fix of the problem on the line
-| *Shift+F8* | Ask the AI to fix the problem on the line
+| *Shift+F8* | Ask the AI to fix the problem on the line, or what fails on it 
at runtime
 | *F7* | Diff view (unsaved changes)
 | *Ctrl+S* | Save and continue editing
 | *F5* | Save and close
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPrompt.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPrompt.java
index d8b19f75b920..0f5015772de4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPrompt.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPrompt.java
@@ -34,6 +34,29 @@ final class AiFixPrompt {
      * @param line      the line of the problem, 1-based
      */
     static String of(Path directory, Path file, int line, String problem, 
String lineText) {
+        return "Fix the problem on line " + line + " of " + nameOf(directory, 
file) + ": " + problem + "\n"
+               + "The line is: " + lineText.strip() + "\n"
+               + "Change only what this problem is about, with 
camel_edit_file, then check the file with"
+               + " camel_validate_source.";
+    }
+
+    /**
+     * The question for a line that fails at runtime (Shift+F8 on a line with 
failures in the live run data). The cause
+     * may lie elsewhere than on the line (such as a direct: endpoint no route 
consumes), so the AI is asked to find it
+     * first, and dev mode reloads the fix once it is saved.
+     *
+     * @param failure how many exchanges failed on the line, and the exception 
of the last one when known
+     */
+    static String ofFailure(Path directory, Path file, int line, String 
failure, String lineText) {
+        return "Exchanges fail at runtime on line " + line + " of " + 
nameOf(directory, file) + ": " + failure + "\n"
+               + "The line is: " + lineText.strip() + "\n"
+               + "Find the cause from the integration's errors and log, then 
fix it with camel_edit_file (the fix may"
+               + " belong on another line or in another route) and check the 
file with camel_validate_source."
+               + " Dev mode reloads the file when it is saved.";
+    }
+
+    /** The name of the file relative to the project directory, when it is 
inside it. */
+    private static String nameOf(Path directory, Path file) {
         String name = file.toString();
         if (directory != null) {
             Path dir = directory.toAbsolutePath().normalize();
@@ -42,9 +65,6 @@ final class AiFixPrompt {
                 name = dir.relativize(abs).toString();
             }
         }
-        return "Fix the problem on line " + line + " of " + name + ": " + 
problem + "\n"
-               + "The line is: " + lineText.strip() + "\n"
-               + "Change only what this problem is about, with 
camel_edit_file, then check the file with"
-               + " camel_validate_source.";
+        return name;
     }
 }
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 315ffc1a9e96..cf7fa9aa6d7d 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
@@ -37,6 +37,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.function.BiConsumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import dev.tamboui.buffer.Buffer;
@@ -500,6 +501,18 @@ public class CamelMonitor extends CamelCommand {
         });
     }
 
+    /**
+     * Opens the AI panel with a question for the Source editor's fix with AI 
(Shift+F8), made from the directory of the
+     * selected integration's sources.
+     */
+    private void openAiWithQuestion(Function<Path, String> question) {
+        if (shellPanel.isOpen()) {
+            shellPanel.close();
+        }
+        Path dir = mcpFacade != null ? mcpFacade.getSelectedSourceDirectory() 
: null;
+        aiPanel.openWithQuestion(question.apply(dir));
+    }
+
     /**
      * Wires the shared MonitorContext callbacks and connects the shell and AI 
panels.
      */
@@ -507,12 +520,16 @@ public class CamelMonitor extends CamelCommand {
         ctx.notificationCallback = (msg, error) -> setNotification(msg, error);
         ctx.openMarkdownCallback = actionsPopup::openMarkdown;
         ctx.openMarkdownAtCallback = actionsPopup::openMarkdownAt;
-        ctx.askAiCallback = (file, line, problem, lineText) -> {
-            if (shellPanel.isOpen()) {
-                shellPanel.close();
+        ctx.askAiCallback = new MonitorContext.AskAi() {
+            @Override
+            public void fixProblem(Path file, int line, String problem, String 
lineText) {
+                openAiWithQuestion(dir -> AiFixPrompt.of(dir, file, line, 
problem, lineText));
+            }
+
+            @Override
+            public void fixFailure(Path file, int line, String failure, String 
lineText) {
+                openAiWithQuestion(dir -> AiFixPrompt.ofFailure(dir, file, 
line, failure, lineText));
             }
-            Path dir = mcpFacade != null ? 
mcpFacade.getSelectedSourceDirectory() : null;
-            aiPanel.openWithQuestion(AiFixPrompt.of(dir, file, line, problem, 
lineText));
         };
         ctx.projectOverviewCallback = () -> {
             if (shellPanel.isOpen()) {
@@ -2929,7 +2946,7 @@ public class CamelMonitor extends CamelCommand {
             rightWidth = 0;
             minGap = 0;
             // Drop secondary F-key hints (F2/F3/F6) before tab-specific 
action hints.
-            hintsWidth = dropFKeyHints(spans, fKeyTotal, hintsWidth, 
area.width());
+            hintsWidth = dropFKeyHints(spans, fKeyTotal, tabFKeySpans, 
hintsWidth, area.width());
             // Then drop tab-specific hints from the tail, keeping at least 4 
spans
             while (spans.size() > 4 && hintsWidth > area.width()) {
                 Span labelSpan = spans.remove(spans.size() - 1);
@@ -2948,6 +2965,9 @@ public class CamelMonitor extends CamelCommand {
         frame.renderWidget(Paragraph.from(Line.from(spans)), area);
     }
 
+    // how many of the F-key hint spans of the footer are the screen's own 
(after F1/F2/F10)
+    private int tabFKeySpans;
+
     private int insertFKeyHints(List<Span> spans) {
         int insertPos = Math.min(2, spans.size());
         List<Span> fKeySpans = new ArrayList<>();
@@ -2958,9 +2978,11 @@ public class CamelMonitor extends CamelCommand {
         }
         hint(fKeySpans, "F2", "actions");
         hint(fKeySpans, "F10", "run");
+        int globalFKeys = fKeySpans.size();
         if (tab != null) {
             tab.renderFKeyHints(fKeySpans);
         }
+        tabFKeySpans = fKeySpans.size() - globalFKeys;
         spans.addAll(insertPos, fKeySpans);
         // Return total F-key span count. The footer drop loop uses this to 
remove pairs from
         // the tail, stopping before the first pair (F1 help when present).
@@ -2979,7 +3001,27 @@ public class CamelMonitor extends CamelCommand {
      * @return            the rendered width of {@code spans} after dropping
      */
     static int dropFKeyHints(List<Span> spans, int fKeyTotal, int hintsWidth, 
int available) {
-        while (fKeyTotal > 2 && hintsWidth > available) {
+        return dropFKeyHints(spans, fKeyTotal, 0, hintsWidth, available);
+    }
+
+    /**
+     * As {@link #dropFKeyHints(List, int, int, int)}, with the screen's own 
F-key hints (such as Shift+F8 fix with AI
+     * or F12 file actions) after the global ones: the global F2/F10 are 
dropped first, as they are the same on every
+     * screen, and then the screen's own from the tail.
+     *
+     * @param tabFKeys how many of the F-key spans are the screen's own, at 
the end of the F-key spans
+     */
+    static int dropFKeyHints(List<Span> spans, int fKeyTotal, int tabFKeys, 
int hintsWidth, int available) {
+        int globalTotal = fKeyTotal - Math.max(0, tabFKeys);
+        while (globalTotal > 2 && hintsWidth > available) {
+            // the last global pair: its key span is at index globalTotal (the 
F-keys start at 2)
+            Span labelSpan = spans.remove(globalTotal + 1);
+            Span keySpan = spans.remove(globalTotal);
+            hintsWidth -= keySpan.width() + labelSpan.width();
+            globalTotal -= 2;
+            fKeyTotal -= 2;
+        }
+        while (fKeyTotal > Math.max(2, globalTotal) && hintsWidth > available) 
{
             Span labelSpan = spans.remove(fKeyTotal + 1);
             Span keySpan = spans.remove(fKeyTotal);
             hintsWidth -= keySpan.width() + labelSpan.width();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index fe735789bf79..0a4d1d7a874a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -29,6 +30,7 @@ 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.tui.event.MouseEvent;
 import dev.tamboui.tui.event.MouseEventKind;
@@ -93,6 +95,11 @@ class ErrorsTab extends AbstractTableTab {
 
     @Override
     protected boolean handleTabKeyEvent(KeyEvent ke) {
+        if (ke.isKey(KeyCode.F8) && ke.hasShift()) {
+            // fix with AI: the selected error, on the source line it happened 
at
+            askAiToFixSelectedError();
+            return true;
+        }
         if (diagram.isShowDiagram() && diagram.isHistoryMode() && 
diagram.hasHistoryData()) {
             if (diagram.isHistoryTopologyMode()) {
                 if (ke.isUp()) {
@@ -468,6 +475,48 @@ class ErrorsTab extends AbstractTableTab {
         hintShowBhpv(spans, showBody, showHeaders, showProperties, 
showVariables);
     }
 
+    @Override
+    public void renderFKeyHints(List<Span> spans) {
+        // with the global F-keys (after Esc back), as fix with AI is an F-key 
of every screen that has it
+        if (!diagram.isShowDiagram() && ctx.askAiCallback != null && 
selectedError() != null) {
+            hint(spans, "Shift+F8", "fix with AI");
+        }
+    }
+
+    /** The error selected in the table, or null. */
+    private ErrorInfo selectedError() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        List<ErrorInfo> sorted = applyFilter(info.errors);
+        Integer sel = tableState.selected();
+        return sel != null && sel >= 0 && sel < sorted.size() ? 
sorted.get(sel) : null;
+    }
+
+    /**
+     * Asks the AI to fix the selected error (Shift+F8), as the Source editor 
does for a line that fails at runtime: the
+     * source line of the processor it happened at, and its exception.
+     */
+    private void askAiToFixSelectedError() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        ErrorInfo error = selectedError();
+        if (info == null || error == null || ctx.askAiCallback == null) {
+            return;
+        }
+        String source = RuntimeFailures.sourceOf(info.routes, error);
+        Path file = 
RuntimeFailures.fileOf(FilesBrowser.resolveSourceDirectory(info), source);
+        int line = file != null ? LiveRunLines.lineOf(source, 
file.getFileName().toString()) : -1;
+        if (line < 0) {
+            if (ctx.notificationCallback != null) {
+                ctx.notificationCallback.accept("The source line of this error 
is not known, ask the AI with F8", false);
+            }
+            return;
+        }
+        ctx.askAiCallback.fixFailure(file, line + 1, 
RuntimeFailures.failureOf(error),
+                RuntimeFailures.lineText(file, line));
+    }
+
     /** How often this kind of error happened, and how long it has been going 
on (CAMEL-24911). */
     private static void addRepeatLine(List<Line> lines, ErrorInfo ei) {
         if (ei.repeatCount <= 1) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
index 3c575c50a599..ea748e53d236 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
@@ -55,30 +55,39 @@ final class LiveRunLines {
 
     /** Adds what a processor did to its line: totals and failures summed, the 
mean time weighted by the exchanges. */
     private static void add(Map<Integer, long[]> sums, String name, String 
source, long total, long failed, long mean) {
-        if (source == null) {
+        int line = lineOf(source, name);
+        if (line < 0) {
             return;
         }
+        long[] s = sums.computeIfAbsent(line, k -> new long[3]);
+        s[0] += total;
+        s[1] += failed;
+        s[2] += Math.max(0, mean) * total;
+    }
+
+    /**
+     * The 0-based line of a source location (file:line) in the file of the 
given name, or -1 when the location is of
+     * another file or has no line.
+     */
+    static int lineOf(String source, String name) {
+        if (source == null || name == null) {
+            return -1;
+        }
         int colon = source.lastIndexOf(':');
         if (colon <= 0 || !name.equals(nameOf(source.substring(0, colon)))) {
-            return;
+            return -1;
         }
         int line;
         try {
             line = Integer.parseInt(source.substring(colon + 1).trim());
         } catch (NumberFormatException e) {
-            return;
-        }
-        if (line <= 0) {
-            return;
+            return -1;
         }
-        long[] s = sums.computeIfAbsent(line - 1, k -> new long[3]);
-        s[0] += total;
-        s[1] += failed;
-        s[2] += Math.max(0, mean) * total;
+        return line > 0 ? line - 1 : -1;
     }
 
     /** The file name of a location: file:/work/OrderRoute.java, 
classpath:OrderRoute.java and OrderRoute.java alike. */
-    private static String nameOf(String location) {
+    static String nameOf(String location) {
         if (location == null || location.isBlank()) {
             return null;
         }
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 ec890d4fcf62..bb31fa8400f0 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
@@ -209,9 +209,21 @@ class MonitorContext {
         return CommandLineHelper.getCamelDir().resolve(pid + "-trace.json");
     }
 
-    /** Asks the AI to fix a problem of a source file. */
+    Path getErrorFile(String pid) {
+        return CommandLineHelper.getCamelDir().resolve(pid + "-error.json");
+    }
+
+    /** Asks the AI to fix a problem of a source file, or a line that fails at 
runtime. */
     @FunctionalInterface
     interface AskAi {
         void fixProblem(Path file, int line, String problem, String lineText);
+
+        /**
+         * Asks the AI to fix a line whose processors fail at runtime 
(Shift+F8 on a line with failures in the live run
+         * data): the failure says how many exchanges failed and the exception 
of the last one.
+         */
+        default void fixFailure(Path file, int line, String failure, String 
lineText) {
+            fixProblem(file, line, failure, lineText);
+        }
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeFailures.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeFailures.java
new file mode 100644
index 000000000000..730e77561ef0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeFailures.java
@@ -0,0 +1,168 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * The runtime failures of a running integration for fix with AI (Shift+F8 in 
the Source editor and the Errors tab): the
+ * source line an error of the error registry happened on, found through the 
source location of its processor
+ * (file:line, as the live run data of the Source tab), and the exception of 
it as one line.
+ */
+final class RuntimeFailures {
+
+    private static final int MAX_MESSAGE = 300;
+
+    private RuntimeFailures() {
+    }
+
+    /** The errors of an integration, read from its error file (written by the 
running integration); empty if none. */
+    static List<ErrorInfo> load(String pid, Function<String, Path> 
errorFileResolver) {
+        if (pid == null) {
+            return List.of();
+        }
+        try {
+            JsonObject root = TuiHelper.loadStatus(Long.parseLong(pid), 
errorFileResolver);
+            return root != null ? StatusParser.parseErrors(root) : List.of();
+        } catch (NumberFormatException e) {
+            return List.of();
+        }
+    }
+
+    /**
+     * The source location (file:line) of the processor an error happened at, 
else of its route; null when not known.
+     */
+    static String sourceOf(List<RouteInfo> routes, ErrorInfo error) {
+        if (routes == null || error == null || error.routeId == null) {
+            return null;
+        }
+        for (RouteInfo r : routes) {
+            if (!error.routeId.equals(r.routeId)) {
+                continue;
+            }
+            if (error.nodeId != null) {
+                for (ProcessorInfo p : r.processors) {
+                    if (error.nodeId.equals(p.id) && p.source != null) {
+                        return p.source;
+                    }
+                }
+            }
+            return r.source;
+        }
+        return null;
+    }
+
+    /** The exception of an error as one line: the simple class name and the 
first line of the message. */
+    static String describe(ErrorInfo error) {
+        String message = error.exceptionMessage;
+        if (message != null) {
+            int nl = message.indexOf('\n');
+            message = (nl >= 0 ? message.substring(0, nl) : message).strip();
+            if (message.length() > MAX_MESSAGE) {
+                message = message.substring(0, MAX_MESSAGE) + "...";
+            }
+        }
+        String type = error.exceptionType;
+        if (type == null) {
+            return message;
+        }
+        String simple = type.substring(type.lastIndexOf('.') + 1);
+        return message != null && !message.isEmpty() ? simple + ": " + message 
: simple;
+    }
+
+    /**
+     * The exception of the last error on a 0-based line of a file, or null 
when no error of the error registry is in
+     * the file. A failure also counts on the lines of the route and the EIPs 
around the processor it happened at (the
+     * from, a choice), so when no error is on the line itself, the last error 
in the file is given with its line.
+     */
+    static String lastFailure(List<RouteInfo> routes, List<ErrorInfo> errors, 
String filePath, int line) {
+        String name = LiveRunLines.nameOf(filePath);
+        ErrorInfo onLine = null;
+        ErrorInfo inFile = null;
+        int inFileLine = -1;
+        for (ErrorInfo e : errors) {
+            int at = LiveRunLines.lineOf(sourceOf(routes, e), name);
+            if (at == line && (onLine == null || e.timestamp > 
onLine.timestamp)) {
+                onLine = e;
+            } else if (at >= 0 && (inFile == null || e.timestamp > 
inFile.timestamp)) {
+                inFile = e;
+                inFileLine = at;
+            }
+        }
+        if (onLine != null) {
+            return describe(onLine);
+        }
+        return inFile != null ? describe(inFile) + " (at line " + (inFileLine 
+ 1) + ")" : null;
+    }
+
+    /**
+     * The failure of an error for the AI: its exception, and how many times 
it repeated when it did.
+     */
+    static String failureOf(ErrorInfo error) {
+        String answer = describe(error);
+        if (answer == null) {
+            answer = "the exchange failed";
+        }
+        return error.repeatCount > 1 ? answer + " (" + error.repeatCount + " 
times)" : answer;
+    }
+
+    /**
+     * The file of a source location (file:line) in the source directory of 
the integration, or null when it is not
+     * there.
+     */
+    static Path fileOf(Path sourceDir, String source) {
+        if (sourceDir == null || source == null) {
+            return null;
+        }
+        int colon = source.lastIndexOf(':');
+        String name = LiveRunLines.nameOf(colon > 0 ? source.substring(0, 
colon) : source);
+        if (name == null) {
+            return null;
+        }
+        Path direct = sourceDir.resolve(name);
+        if (Files.isRegularFile(direct)) {
+            return direct;
+        }
+        // a route in a folder of the project (src/main/resources/camel and 
alike)
+        try (var paths = Files.walk(sourceDir, 6)) {
+            return paths.filter(p -> Files.isRegularFile(p) && 
name.equals(p.getFileName().toString()))
+                    .filter(p -> {
+                        String path = p.toString().replace('\\', '/');
+                        return !path.contains("/target/") && 
!path.contains("/.camel-jbang");
+                    })
+                    .findFirst().orElse(null);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /** The text of a 0-based line of a file, or empty when it cannot be read. 
*/
+    static String lineText(Path file, int line) {
+        try {
+            List<String> lines = Files.readAllLines(file);
+            return line >= 0 && line < lines.size() ? lines.get(line) : "";
+        } catch (IOException e) {
+            return "";
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index d05c6f25b018..3e0e58454bdc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -156,6 +156,13 @@ class SourceTab extends AbstractTab {
             IntegrationInfo info = ctx.findSelectedIntegration();
             return info != null ? LiveRunLines.of(info.routes, path) : 
Map.of();
         });
+        // the exception of the last failure on a line, for fix with AI 
(Shift+F8) on a line that fails at runtime
+        sourceViewer.setLineFailures((path, line) -> {
+            IntegrationInfo info = ctx.findSelectedIntegration();
+            return info != null
+                    ? RuntimeFailures.lastFailure(info.routes, 
RuntimeFailures.load(info.pid, ctx::getErrorFile), path, line)
+                    : null;
+        });
         sourceViewer.setOnFileLoaded(p -> {
             if (hasJumpLinks(p)) {
                 sourceViewer.setJumpLinks(computeJumpLinks(p));
@@ -480,6 +487,9 @@ class SourceTab extends AbstractTab {
         if (!fileActionsPopup.isVisible() && !(focusOnViewer && 
sourceViewer.isVisible())) {
             TuiHelper.hint(spans, "F12", "file actions");
         }
+        if (focusOnViewer && sourceViewer.showsFixWithAiHint()) {
+            TuiHelper.hint(spans, "Shift+F8", "fix with AI");
+        }
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 62ad7cfc51ee..ecbd318dfc39 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -141,6 +141,12 @@ class SourceViewer {
         Map<Integer, LiveLine> lines(String filePath);
     }
 
+    /** The last runtime failure of a line of a file, by its path and 0-based 
line: the exception, or null. */
+    @FunctionalInterface
+    interface LineFailures {
+        String lastFailure(String filePath, int line);
+    }
+
     private boolean visible;
     private List<String> lines = Collections.emptyList();
     private List<JsonObject> codeData = Collections.emptyList();
@@ -211,6 +217,7 @@ class SourceViewer {
     private EndpointValidator simpleValidator;
     private EndpointValidator routeValidator;
     private LiveRunData liveRunData;
+    private LineFailures lineFailures;
     private Map<Integer, LiveLine> liveLines = Collections.emptyMap();
     private long liveLinesTime;
     private int liveTotalWidth;
@@ -319,6 +326,37 @@ class SourceViewer {
         this.liveRunData = liveRunData;
     }
 
+    void setLineFailures(LineFailures lineFailures) {
+        this.lineFailures = lineFailures;
+    }
+
+    /**
+     * The runtime failure of a 0-based line for fix with AI (Shift+F8): how 
many exchanges failed on the line in the
+     * live run data, and the exception of the last one when known; null when 
nothing failed on it, or when the file is
+     * edited and not saved (the live run data is of the saved file).
+     */
+    String runtimeFailure(int row) {
+        if (!failsAtRuntime(row)) {
+            return null;
+        }
+        LiveLine live = liveLines().get(row);
+        String answer = live.failed() + (live.failed() == 1 ? " exchange" : " 
exchanges") + " failed on this line";
+        String last = lineFailures != null && loadedFilePath != null ? 
lineFailures.lastFailure(loadedFilePath, row) : null;
+        return last != null ? answer + ", the last with " + last : answer;
+    }
+
+    /**
+     * Whether processors on a 0-based line failed in the live run data, while 
the file is not edited (cheap: the hint
+     * of each frame asks it; the exception is only read on Shift+F8).
+     */
+    private boolean failsAtRuntime(int row) {
+        if (dirty || row < 0) {
+            return false;
+        }
+        LiveLine live = liveLines().get(row);
+        return live != null && live.failed() > 0;
+    }
+
     /**
      * The live run data as a column of its own after the line numbers, so the 
numbers line up whatever the length of
      * the code: the exchanges right-aligned, then the failures and the mean 
time when any line has them, blank on the
@@ -742,6 +780,11 @@ class SourceViewer {
             goToNextProblem(viewErrors, selectedLine);
             return true;
         }
+        if (ke.isKey(KeyCode.F8) && ke.hasShift()) {
+            // fix with AI: the problem of the selected line, or what fails on 
it at runtime
+            askAiToFix();
+            return true;
+        }
         if (isMarkdownFile && ke.isChar(' ')) {
             markdownMode = !markdownMode;
             return true;
@@ -2057,14 +2100,24 @@ class SourceViewer {
     }
 
     /**
-     * Asks the AI to fix the problem of the cursor line (Shift+F8): the file 
is saved as it is in the editor, which the
-     * AI is about to change, edit mode left so the editor shows what the AI 
sees, and the AI panel opened with the
-     * question in its input, for the user to send with Enter or change first.
+     * Asks the AI to fix the problem of the cursor line (Shift+F8), or when 
it has none, the line whose processors fail
+     * at runtime: the file is saved as it is in the editor, which the AI is 
about to change, edit mode left so the
+     * editor shows what the AI sees, and the AI panel opened with the 
question in its input, for the user to send with
+     * Enter or change first.
      */
     private void askAiToFix() {
+        if (askAi == null || editableFile == null) {
+            return;
+        }
+        if (!editMode) {
+            askAiToFixInView();
+            return;
+        }
         int row = editState.cursorRow();
         String problem = visibleInlineErrors().get(row);
-        if (askAi == null || editableFile == null || problem == null) {
+        // a runtime failure is only known for the saved file (null while 
dirty), so it needs no save
+        String failure = problem == null ? runtimeFailure(row) : null;
+        if (problem == null && failure == null) {
             return;
         }
         String lineText = editState.getLine(row);
@@ -2081,7 +2134,52 @@ class SourceViewer {
         exitEditMode();
         loadFile(file);
         goToLine(row);
-        askAi.fixProblem(file, row + 1, problem, lineText);
+        if (problem != null) {
+            askAi.fixProblem(file, row + 1, problem, lineText);
+        } else {
+            askAi.fixFailure(file, row + 1, failure, lineText);
+        }
+    }
+
+    /**
+     * Shift+F8 in the view, as in the editor: the problem of the selected 
line, or else its runtime failure, while the
+     * view shows the file's own source (not the route converted to another 
DSL).
+     */
+    private void askAiToFixInView() {
+        int row = selectedLine;
+        if (row < 0 || row >= lines.size() || !showsOwnSource()) {
+            return;
+        }
+        // the view renders the lines (line numbers, live data), so the text 
comes from the file, which the view shows
+        String lineText = RuntimeFailures.lineText(editableFile, row);
+        String problem = viewErrors.get(row);
+        if (problem != null) {
+            askAi.fixProblem(editableFile, row + 1, problem, lineText);
+            return;
+        }
+        String failure = runtimeFailure(row);
+        if (failure != null) {
+            askAi.fixFailure(editableFile, row + 1, failure, lineText);
+        }
+    }
+
+    /** Whether the view shows the file's own source, and not its route 
converted to another DSL (Space). */
+    private boolean showsOwnSource() {
+        return !markdownMode && (currentFormat == null || 
currentFormat.equals(originalFormat));
+    }
+
+    /**
+     * Whether the view shows the fix with AI hint (Shift+F8) for the selected 
line: the Source tab puts it with the
+     * global F-keys.
+     */
+    boolean showsFixWithAiHint() {
+        return visible && !editMode && showsOwnSource() && 
canAskAiToFix(selectedLine, viewErrors);
+    }
+
+    /** Whether Shift+F8 (fix with AI) has something to ask about on a 0-based 
line: a problem, or a runtime failure. */
+    private boolean canAskAiToFix(int row, Map<Integer, String> problems) {
+        return askAi != null && editableFile != null && row >= 0
+                && (problems.containsKey(row) || failsAtRuntime(row));
     }
 
     /** Applies the fix of the problem on the line of the cursor (Shift+F9), 
and checks the content again right away. */
@@ -2984,7 +3082,7 @@ class SourceViewer {
             if (cursorFix() != null) {
                 TuiHelper.hint(spans, "Shift+F9", "fix");
             }
-            if (askAi != null && 
visibleInlineErrors().containsKey(editState.cursorRow())) {
+            if (canAskAiToFix(editState.cursorRow(), visibleInlineErrors())) {
                 TuiHelper.hint(spans, "Shift+F8", "fix with AI");
             }
             if (isCamelYamlFile()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
index 092308cdcbf9..0ee9f76cec62 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
@@ -149,6 +149,31 @@ class CamelMonitorTest {
         assertEquals(width(spans), newWidth, "returned width must match the 
remaining spans");
     }
 
+    @Test
+    void dropFKeyHintsDropsTheGlobalOnesBeforeTheScreensOwn() {
+        // tab hint + F1/F2/F10 + the screen's own Shift+F8, fKeyTotal = 8 
spans of which 2 are the screen's
+        List<Span> spans = footer("Esc", "back");
+        hint(spans, "F1", "help");
+        hint(spans, "F2", "actions");
+        hint(spans, "F10", "run");
+        hint(spans, "Shift+F8", "fix with AI");
+        int width = width(spans);
+
+        int available = width - pairWidth(spans, "F10") - pairWidth(spans, 
"F2");
+        int newWidth = CamelMonitor.dropFKeyHints(spans, 8, 2, width, 
available);
+
+        assertTrue(containsKey(spans, "Shift+F8"), "the screen's own F-key 
stays while global ones can go");
+        assertTrue(containsKey(spans, "F1"), "F1 (help) must be preserved");
+        assertFalse(containsKey(spans, "F10"), "F10 is dropped first");
+        assertFalse(containsKey(spans, "F2"), "F2 is dropped next");
+        assertEquals(width(spans), newWidth, "returned width must match the 
remaining spans");
+
+        // under heavy overflow the screen's own goes too, F1 stays
+        CamelMonitor.dropFKeyHints(spans, 4, 2, width(spans), 1);
+        assertFalse(containsKey(spans, "Shift+F8"));
+        assertTrue(containsKey(spans, "F1"));
+    }
+
     @Test
     void dropFKeyHintsLeavesFooterUntouchedWhenItFits() {
         List<Span> spans = footer("Enter", "open");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FixRuntimeFailureTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FixRuntimeFailureTest.java
new file mode 100644
index 000000000000..f3a9669c1706
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FixRuntimeFailureTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Fix with AI (Shift+F8) on a line that fails at runtime: in the Source view 
and editor, from the live run data and the
+ * error registry (CAMEL-25358).
+ */
+class FixRuntimeFailureTest {
+
+    private static final String ROUTE = """
+            - route:
+                id: orders
+                from:
+                  uri: timer:orders
+                  steps:
+                    - choice:
+                        when:
+                          - simple: "${header.amount} > 100"
+                            steps:
+                              - to:
+                                  uri: direct:big-orders
+                    - log:
+                        message: "${body}"
+            """;
+
+    private static final int TO_LINE = 10; // 0-based line of uri: 
direct:big-orders
+
+    private static final String EXCEPTION
+            = "DirectConsumerNotAvailableException: No consumers available on 
endpoint: direct://big-orders";
+
+    @TempDir
+    Path tempDir;
+
+    private final AtomicReference<String> problem = new AtomicReference<>();
+    private final AtomicReference<String> failure = new AtomicReference<>();
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    private SourceViewer viewer(Path file, long failed) {
+        SourceViewer viewer = new SourceViewer();
+        viewer.setAskAi(new MonitorContext.AskAi() {
+            @Override
+            public void fixProblem(Path f, int line, String p, String text) {
+                problem.set(f.getFileName() + ":" + line + ": " + p);
+            }
+
+            @Override
+            public void fixFailure(Path f, int line, String fail, String text) 
{
+                failure.set(f.getFileName() + ":" + line + ": " + fail + " | " 
+ text.strip());
+            }
+        });
+        viewer.setLiveRunData(path -> Map.of(TO_LINE, new 
SourceViewer.LiveLine(5, failed, 2)));
+        viewer.setLineFailures((path, line) -> line == TO_LINE ? EXCEPTION : 
null);
+        viewer.loadFile(file);
+        viewer.refreshLiveLinesForTesting();
+        return viewer;
+    }
+
+    private Path routeFile() throws Exception {
+        Path file = tempDir.resolve("orders.camel.yaml");
+        Files.writeString(file, ROUTE, StandardCharsets.UTF_8);
+        return file;
+    }
+
+    @Test
+    void theQuestionToFixARuntimeFailure() {
+        String q = AiFixPrompt.ofFailure(Path.of("/work/app"), 
Path.of("/work/app/orders.camel.yaml"), 11,
+                "3 exchanges failed on this line, the last with " + EXCEPTION, 
"            uri: direct:big-orders");
+        assertThat(q).startsWith("Exchanges fail at runtime on line 11 of 
orders.camel.yaml: 3 exchanges failed")
+                .contains("The line is: uri: direct:big-orders")
+                .contains("errors and log").contains("another 
route").contains("camel_edit_file");
+    }
+
+    @Test
+    void shiftF8InTheViewAsksTheAiToFixTheFailingLine() throws Exception {
+        SourceViewer viewer = viewer(routeFile(), 3);
+        viewer.goToLine(TO_LINE);
+
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F8, KeyModifiers.SHIFT));
+
+        assertThat(failure.get()).isEqualTo("orders.camel.yaml:11: 3 exchanges 
failed on this line, the last with "
+                                            + EXCEPTION + " | uri: 
direct:big-orders");
+        assertThat(problem.get()).isNull();
+    }
+
+    @Test
+    void shiftF8InTheEditorAsksTheAiToFixTheFailingLine() throws Exception {
+        SourceViewer viewer = viewer(routeFile(), 1);
+        viewer.enterEditMode();
+        for (int i = 0; i < TO_LINE; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F8, KeyModifiers.SHIFT));
+
+        assertThat(failure.get()).startsWith("orders.camel.yaml:11: 1 exchange 
failed on this line, the last with ");
+        assertThat(viewer.isEditMode()).isFalse();
+    }
+
+    @Test
+    void theHintShowsOnAFailingLineOfTheView() throws Exception {
+        SourceViewer viewer = viewer(routeFile(), 3);
+        viewer.goToLine(TO_LINE);
+        assertThat(viewer.showsFixWithAiHint()).isTrue();
+
+        viewer.goToLine(TO_LINE + 2);
+        assertThat(viewer.showsFixWithAiHint()).isFalse();
+    }
+
+    @Test
+    void aLineWithoutFailuresAsksNothing() throws Exception {
+        SourceViewer viewer = viewer(routeFile(), 3);
+        viewer.goToLine(TO_LINE + 2);
+
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F8, KeyModifiers.SHIFT));
+
+        assertThat(failure.get()).isNull();
+        assertThat(problem.get()).isNull();
+    }
+
+    @Test
+    void anEditedFileHasNoRuntimeFailures() throws Exception {
+        SourceViewer viewer = viewer(routeFile(), 3);
+        viewer.enterEditMode();
+        viewer.handleKeyEvent(KeyEvent.ofChar('x', KeyModifiers.NONE));
+        for (int i = 0; i < TO_LINE; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+
+        // the live run data is of the saved file, so an unsaved edit does not 
use it
+        assertThat(viewer.runtimeFailure(TO_LINE)).isNull();
+    }
+
+    @Test
+    void theErrorOfALineIsFoundThroughItsProcessor() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml:2";
+        ProcessorInfo to = new ProcessorInfo();
+        to.id = "to1";
+        to.source = "file:/work/app/orders.camel.yaml:11";
+        route.processors.add(to);
+        ErrorInfo older = error("to1", 1000, 
"java.lang.IllegalStateException", "older");
+        ErrorInfo last = error("to1", 2000, 
"org.apache.camel.component.direct.DirectConsumerNotAvailableException",
+                "No consumers available on endpoint: 
direct://big-orders\nExchange[123]");
+        ErrorInfo onRoute = error("unknown", 3000, 
"java.lang.RuntimeException", "on the route");
+
+        assertThat(RuntimeFailures.sourceOf(List.of(route), 
last)).isEqualTo("file:/work/app/orders.camel.yaml:11");
+        assertThat(RuntimeFailures.sourceOf(List.of(route), 
onRoute)).isEqualTo("orders.camel.yaml:2");
+        assertThat(RuntimeFailures.lastFailure(List.of(route), List.of(older, 
last, onRoute),
+                "/work/app/orders.camel.yaml", TO_LINE)).isEqualTo(EXCEPTION);
+        assertThat(RuntimeFailures.lastFailure(List.of(route), List.of(older, 
last), "/work/app/other.camel.yaml",
+                TO_LINE)).isNull();
+        // the from line of the route counts the failure too: the last error 
in the file, with its line
+        assertThat(RuntimeFailures.lastFailure(List.of(route), List.of(older, 
last), "/work/app/orders.camel.yaml", 3))
+                .isEqualTo(EXCEPTION + " (at line 11)");
+
+        last.repeatCount = 4;
+        assertThat(RuntimeFailures.failureOf(last)).isEqualTo(EXCEPTION + " (4 
times)");
+    }
+
+    @Test
+    void theFileOfASourceLocationIsInTheSourceDirectory() throws Exception {
+        Path routes = 
Files.createDirectories(tempDir.resolve("src/main/resources/camel"));
+        Files.writeString(routes.resolve("orders.camel.yaml"), ROUTE, 
StandardCharsets.UTF_8);
+
+        Path file = RuntimeFailures.fileOf(tempDir, 
"classpath:camel/orders.camel.yaml:11");
+
+        assertThat(file).isEqualTo(routes.resolve("orders.camel.yaml"));
+        assertThat(RuntimeFailures.lineText(file, TO_LINE)).contains("uri: 
direct:big-orders");
+        assertThat(RuntimeFailures.fileOf(tempDir, 
"missing.camel.yaml:3")).isNull();
+    }
+
+    private static ErrorInfo error(String nodeId, long timestamp, String type, 
String message) {
+        ErrorInfo e = new ErrorInfo();
+        e.routeId = "orders";
+        e.nodeId = nodeId;
+        e.timestamp = timestamp;
+        e.exceptionType = type;
+        e.exceptionMessage = message;
+        return e;
+    }
+}

Reply via email to