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 c99ef14a8359 chore: camel-jbang - fix with AI quotes the whole step
around the line (#27654)
c99ef14a8359 is described below
commit c99ef14a83591418ae167598daa855263705c218
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 10 12:32:21 2026 +0200
chore: camel-jbang - fix with AI quotes the whole step around the line
(#27654)
The question Shift+F8 writes quoted only the reported line. In the
canonical YAML form that line is "- to:" and the endpoint is on the
lines below it, so the model did not see what failed. The question now
quotes the whole step (the lines below indented deeper, up to 8), with 2
lines of context before and after, numbered and the line marked with >,
as in a diff. It falls back to the single line when the file cannot be
read.
The chat showed a code block inside the quoted question as empty lines;
the question is now quoted line by line and its code block is shown
outside the quote.
Closes #27654
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-source-editor.adoc | 6 +-
.../dsl/jbang/core/commands/tui/AiFixPrompt.java | 83 +++++++++-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 36 ++++-
.../jbang/core/commands/tui/AiFixPromptTest.java | 172 +++++++++++++++++++++
4 files changed, 290 insertions(+), 7 deletions(-)
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 9a09e4645c96..46c87141562f 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
@@ -99,8 +99,10 @@ The same fixes are part of the result of the
`camel_validate_source` MCP tool, s
== Fix with AI
When a problem has no quick fix, *Shift+F8* asks the AI. The file is saved as
it is, and the AI panel opens with the
-question already written: the file, the line, the problem, and how to fix it.
Press *Enter* to send it, or change it
-first.
+question already written: the file, the line, the problem, and how to fix it.
The question quotes the source around
+the line, like a diff: the whole step the line starts (in the canonical YAML
form the endpoint of a `- to:` is on the
+lines below it), up to 8 lines, with 2 lines of context before and after, the
line marked with `>`. Press *Enter* to
+send it, or change it first.
image::jbang/camel-tui-source-fix-with-ai.png[Shift+F8 writes the question for
the AI]
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 4e6775df1a77..a61b6b1ec347 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
@@ -16,15 +16,23 @@
*/
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;
/**
* The question the AI panel is opened with to fix a problem of the Source
editor (Shift+F8): the file, the line, the
- * problem and the text of the line, and what to do with them - fix only that,
with the edit tool, and validate. It is
- * put in the input of the panel, for the user to send with Enter or change
first.
+ * problem and the source around the line, and what to do with them - fix only
that, with the edit tool, and validate.
+ * It is put in the input of the panel, for the user to send with Enter or
change first.
*/
final class AiFixPrompt {
+ /** Lines of context before the line and after its step, as in a diff. */
+ static final int CONTEXT_LINES = 2;
+ /** The most lines of the step that are quoted after the line itself, so a
big block does not flood the question. */
+ static final int MAX_STEP_LINES = 8;
+
private AiFixPrompt() {
}
@@ -35,7 +43,7 @@ final class AiFixPrompt {
*/
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"
+ + source(file, line, lineText) + "\n"
+ "Change only what this problem is about, with
camel_edit_file, then check the file with"
+ " camel_validate_source.";
}
@@ -49,7 +57,7 @@ final class AiFixPrompt {
*/
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"
+ + source(file, line, lineText) + "\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.";
@@ -67,6 +75,73 @@ final class AiFixPrompt {
+ " file with camel_validate_source. Dev mode reloads the file
when it is saved.";
}
+ /**
+ * The source around the line, read from the file: the whole step the line
starts (the lines below it that are
+ * indented deeper, such as the {@code uri} and {@code parameters} of a
YAML {@code - to:}), with a few lines of
+ * context, the line marked with {@code >}. The line alone when the file
cannot be read or has changed.
+ */
+ private static String source(Path file, int line, String lineText) {
+ List<String> lines;
+ try {
+ lines = Files.readAllLines(file);
+ } catch (IOException e) {
+ lines = List.of();
+ }
+ String excerpt = excerpt(lines, line - 1);
+ if (excerpt == null) {
+ return "The line is: " + lineText.strip();
+ }
+ return "The source around it (> marks line " + line + "):\n```\n" +
excerpt + "```";
+ }
+
+ /**
+ * The lines from a little before the row to a little after the end of its
step, numbered, the row marked with
+ * {@code >}; null when the row is not in the lines.
+ *
+ * @param row the row of the line, 0-based
+ */
+ static String excerpt(List<String> lines, int row) {
+ if (row < 0 || row >= lines.size() || lines.get(row).isBlank()) {
+ return null;
+ }
+ int end = stepEnd(lines, row);
+ int from = Math.max(0, row - CONTEXT_LINES);
+ int to = Math.min(lines.size() - 1, end + CONTEXT_LINES);
+ int width = String.valueOf(to + 1).length();
+ StringBuilder sb = new StringBuilder();
+ for (int i = from; i <= to; i++) {
+ String number = String.format("%" + width + "d", i + 1);
+ sb.append(i == row ? "> " : " ").append(number).append(" |
").append(lines.get(i).stripTrailing())
+ .append('\n');
+ }
+ return sb.toString();
+ }
+
+ /** The last row of the step the row starts: the rows below it that are
indented deeper, blank rows inside them. */
+ private static int stepEnd(List<String> lines, int row) {
+ int indent = indentOf(lines.get(row));
+ int end = row;
+ for (int i = row + 1; i < lines.size() && i - row <= MAX_STEP_LINES;
i++) {
+ String text = lines.get(i);
+ if (text.isBlank()) {
+ continue;
+ }
+ if (indentOf(text) <= indent) {
+ break;
+ }
+ end = i;
+ }
+ return end;
+ }
+
+ private static int indentOf(String text) {
+ int i = 0;
+ while (i < text.length() && Character.isWhitespace(text.charAt(i))) {
+ i++;
+ }
+ return i;
+ }
+
/** 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();
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 a5cb703aa530..a19f67764bea 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
@@ -2578,7 +2578,7 @@ class AiPanel {
// as it always directly follows its question.
for (ConversationEntry entry : conversation) {
switch (entry.role()) {
- case USER -> md.append("> ").append(entry.text().replace("\n",
"\n> ")).append("\n\n");
+ case USER ->
md.append(quoteQuestion(entry.text())).append("\n\n");
case ASSISTANT -> {
md.append(toHardBreaks(entry.text())).append("\n\n");
if (entry.note() != null) {
@@ -3900,6 +3900,40 @@ class AiPanel {
|| code == KeyCode.F11 || code == KeyCode.F12;
}
+ /**
+ * The user's question as a blockquote, its lines kept as typed (hard
breaks). A fenced code block in it, such as
+ * the source the fix with AI question quotes, is put outside the quote,
as a code block inside a quote is not
+ * rendered.
+ */
+ static String quoteQuestion(String text) {
+ if (text == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ boolean inFence = false;
+ boolean quoted = false;
+ for (String line : text.split("\n", -1)) {
+ if (line.strip().startsWith("```")) {
+ if (!inFence && quoted) {
+ // end the quote, so the fence starts a code block of its
own
+ sb.append('\n');
+ quoted = false;
+ }
+ sb.append(line.strip()).append('\n');
+ if (inFence) {
+ sb.append('\n');
+ }
+ inFence = !inFence;
+ } else if (inFence) {
+ sb.append(line).append('\n');
+ } else {
+ sb.append(line.isBlank() ? ">" : "> " + line + "
").append('\n');
+ quoted = true;
+ }
+ }
+ return sb.toString().stripTrailing();
+ }
+
private static String toHardBreaks(String text) {
if (text == null) {
return "";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPromptTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPromptTest.java
new file mode 100644
index 000000000000..769569c34415
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPromptTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The source the fix with AI question (Shift+F8) quotes: the whole step of
the line, with context, as in a diff.
+ */
+class AiFixPromptTest {
+
+ // the canonical YAML form puts the endpoint of a step on the lines below
"- to:"
+ private static final String ROUTE = """
+ - route:
+ id: orders
+ from:
+ uri: timer
+ parameters:
+ timerName: orders
+ steps:
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${body} > 100"
+ steps:
+ - to:
+ uri: direct
+ parameters:
+ name: aproval
+ otherwise:
+ steps:
+ - log:
+ message: Order accepted
+ """;
+
+ private static final int TO_ROW = 13; // 0-based row of "- to:"
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void quotesTheWholeStepWithContext() {
+ String excerpt = AiFixPrompt.excerpt(ROUTE.lines().toList(), TO_ROW);
+
+ assertThat(excerpt).isEqualTo("""
+ 12 | expression: "${body} > 100"
+ 13 | steps:
+ > 14 | - to:
+ 15 | uri: direct
+ 16 | parameters:
+ 17 | name: aproval
+ 18 | otherwise:
+ 19 | steps:
+ """);
+ }
+
+ @Test
+ void aBigStepIsCutAfterAFewLines() {
+ List<String> lines = new ArrayList<>(List.of("steps:", " - choice:"));
+ for (int i = 0; i < 20; i++) {
+ lines.add(" branch" + i + ": x");
+ }
+
+ String excerpt = AiFixPrompt.excerpt(lines, 1);
+
+ // the line, MAX_STEP_LINES of its step and CONTEXT_LINES after it
+ assertThat(excerpt.lines()).hasSize(1 + 1 + AiFixPrompt.MAX_STEP_LINES
+ AiFixPrompt.CONTEXT_LINES)
+ .first().isEqualTo(" 1 | steps:");
+ assertThat(excerpt).contains("> 2 | -
choice:").contains("branch9").doesNotContain("branch10");
+ }
+
+ @Test
+ void quotesTheCallsChainedOnTheLinesBelow() {
+ List<String> lines = List.of(
+ " public void configure() {",
+ " from(\"timer:tick?peroid=1000\")",
+ " .log(\"tick\")",
+ " .to(\"seda:out\");",
+ " }",
+ "}");
+
+ String excerpt = AiFixPrompt.excerpt(lines, 1);
+
+ assertThat(excerpt).isEqualTo("""
+ 1 | public void configure() {
+ > 2 | from("timer:tick?peroid=1000")
+ 3 | .log("tick")
+ 4 | .to("seda:out");
+ 5 | }
+ 6 | }
+ """);
+ }
+
+ @Test
+ void noExcerptForABlankOrUnknownRow() {
+ List<String> lines = List.of("a", "", "b");
+
+ assertThat(AiFixPrompt.excerpt(lines, 1)).isNull();
+ assertThat(AiFixPrompt.excerpt(lines, 3)).isNull();
+ assertThat(AiFixPrompt.excerpt(lines, -1)).isNull();
+ }
+
+ @Test
+ void theQuestionQuotesTheSourceOfTheFile() throws Exception {
+ Path file = tempDir.resolve("orders.camel.yaml");
+ Files.writeString(file, ROUTE, StandardCharsets.UTF_8);
+
+ String q = AiFixPrompt.ofFailure(tempDir, file, TO_ROW + 1, "2
exchanges failed on this line", "- to:");
+
+ assertThat(q).startsWith("Exchanges fail at runtime on line 14 of
orders.camel.yaml: 2 exchanges failed")
+ .contains("The source around it (> marks line 14):\n```\n")
+ .contains("> 14 | - to:\n")
+ .contains(" 17 | name: aproval\n")
+ .contains("```\nFind the cause")
+ .doesNotContain("The line is:");
+ }
+
+ @Test
+ void theChatShowsTheQuotedSourceAsACodeBlock() {
+ String md = AiPanel.quoteQuestion("""
+ Exchanges fail at runtime on line 14 of orders.camel.yaml: 2
exchanges failed
+ The source around it (> marks line 14):
+ ```
+ > 14 | - to:
+ 15 | uri: direct
+ ```
+ Find the cause""");
+
+ // the question is quoted line by line, the code block is outside the
quote (not rendered inside one)
+ assertThat(md).isEqualTo("""
+ > Exchanges fail at runtime on line 14 of orders.camel.yaml: 2
exchanges failed \s
+ > The source around it (> marks line 14): \s
+
+ ```
+ > 14 | - to:
+ 15 | uri: direct
+ ```
+
+ > Find the cause""");
+ }
+
+ @Test
+ void theLineAloneWhenTheFileCannotBeRead() {
+ String q = AiFixPrompt.of(tempDir, tempDir.resolve("gone.camel.yaml"),
3, "Unknown option", " uri: timr:x");
+
+ assertThat(q).contains("The line is: uri: timr:x\n");
+ }
+}