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 570d327dd082 chore: camel-jbang - source editor asks the AI to fix the 
problem on a line (#27188)
570d327dd082 is described below

commit 570d327dd082b091c6f02102029fc932c3490204
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 13:50:19 2026 +0200

    chore: camel-jbang - source editor asks the AI to fix the problem on a line 
(#27188)
    
    Shift+F8 in the source editor asks the AI to fix the problem of a line: the 
file is saved and the AI panel opens with the question written.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  4 +-
 .../dsl/jbang/core/commands/tui/AiFixPrompt.java   | 50 ++++++++++++++++++++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 13 ++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 10 +++-
 .../jbang/core/commands/tui/MonitorContext.java    |  7 +++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  1 +
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 54 ++++++++++++++++++++--
 .../src/main/resources/tui/help/source.md          |  3 ++
 .../core/commands/tui/SourceQuickFixTest.java      | 53 +++++++++++++++++++++
 9 files changed, 190 insertions(+), 5 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index e8fcc5faf483..cee7fa671c4e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -267,7 +267,9 @@ the application's code, and a check that cannot know 
something (a value computed
 
 When the problem on the line of the cursor says its fix (an option that is a 
typo of another, an enum value a letter
 off, a `to` that should be a `toD`, a Simple expression `+${key}+` where the 
property placeholder `+{{key}}+` is meant),
-the Error panel shows it and *Shift+F9* applies it; *Ctrl+Z* takes it back. 
`camel_validate_source` gives the same fixes to
+the Error panel shows it and *Shift+F9* applies it; *Ctrl+Z* takes it back. 
*Shift+F8* asks the AI to fix the problem on the
+line instead: the file is saved as it is, and the AI panel opens with the 
question in its input (the file, the line,
+the problem and its text), for you to send with *Enter* or change first. 
`camel_validate_source` gives the same fixes to
 an agent, as `fixes` with the text to find and its replacement.
 
 Validation can be toggled off in the Settings (F2 > Settings).
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
new file mode 100644
index 000000000000..d8b19f75b920
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiFixPrompt.java
@@ -0,0 +1,50 @@
+/*
+ * 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.file.Path;
+
+/**
+ * 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.
+ */
+final class AiFixPrompt {
+
+    private AiFixPrompt() {
+    }
+
+    /**
+     * @param directory the project directory the AI's file tools are relative 
to; null when not known
+     * @param file      the file of the problem
+     * @param line      the line of the problem, 1-based
+     */
+    static String of(Path directory, Path file, int line, String problem, 
String lineText) {
+        String name = file.toString();
+        if (directory != null) {
+            Path dir = directory.toAbsolutePath().normalize();
+            Path abs = file.toAbsolutePath().normalize();
+            if (abs.startsWith(dir)) {
+                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.";
+    }
+}
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 5b312f99f0fa..05142a375076 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
@@ -603,6 +603,19 @@ class AiPanel {
         editQuestionHandler = handler;
     }
 
+    /**
+     * Opens the panel with a question in its input, for the user to send with 
Enter or change first: the fix of a
+     * problem of the Source editor (Shift+F8). Nothing is sent until the user 
says so.
+     */
+    void openWithQuestion(String question) {
+        if (!visible) {
+            open();
+        }
+        statsView = false;
+        scrollOffset = 0;
+        replaceInputBuffer(question);
+    }
+
     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 aea932c473bc..8e2e95da2f10 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
@@ -496,6 +496,13 @@ 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();
+            }
+            Path dir = mcpFacade != null ? 
mcpFacade.getSelectedSourceDirectory() : null;
+            aiPanel.openWithQuestion(AiFixPrompt.of(dir, file, line, problem, 
lineText));
+        };
         ctx.projectOverviewCallback = () -> {
             if (shellPanel.isOpen()) {
                 shellPanel.close();
@@ -1453,7 +1460,8 @@ public class CamelMonitor extends CamelCommand {
             }
             return true;
         }
-        if (ke.isKey(KeyCode.F8)) {
+        if (ke.isKey(KeyCode.F8) && !ke.hasShift()) {
+            // Shift+F8 is the Source editor's: fix the problem of the line 
with AI
             if (aiPanel.isOpen()) {
                 aiPanel.close();
             } else {
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 a2955e750478..32fb08228c85 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
@@ -77,6 +77,8 @@ class MonitorContext {
 
     /** Starts the AI project overview (CAMEL-25143) in the AI panel, which 
opens to show how it goes. */
     Runnable projectOverviewCallback;
+    /** Opens the AI panel with a question in its input (the fix of a problem 
of the Source editor, Shift+F8). */
+    AskAi askAiCallback;
     OpenOptionsCallback openOptionsCallback;
     OpenOptionsCallback openCatalogDocCallback;
 
@@ -200,4 +202,9 @@ class MonitorContext {
         return CommandLineHelper.getCamelDir().resolve(pid + "-trace.json");
     }
 
+    /** Asks the AI to fix a problem of a source file. */
+    @FunctionalInterface
+    interface AskAi {
+        void fixProblem(Path file, int line, String problem, String lineText);
+    }
 }
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 471d52575891..5bb40ab14646 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
@@ -842,6 +842,7 @@ class SourceTab extends AbstractTab {
         boolean routeFile = !SourceEditAssist.isYamlFile(filePath)
                 && (isJavaRouteFile(filePath) || isCamelSourceFile(filePath));
         sourceViewer.setRouteValidator(routeFile ? content -> 
assist.validateRoutes(filePath, content) : null);
+        sourceViewer.setAskAi(ctx.askAiCallback);
         String name = filePath.getFileName().toString();
         sourceViewer.setUriCompletion(!routeFile ? null : 
name.endsWith(".java") ? "java" : "xml");
         if (isCamelSourceFile(filePath)) {
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 0295c4e6739c..a9b82841230d 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
@@ -209,6 +209,7 @@ class SourceViewer {
     private int liveFailedWidth;
     private int liveMeanWidth;
     private String liveWidthsFile;
+    private MonitorContext.AskAi askAi;
     /** java or xml: Tab completes the endpoint uris of a Java or XML route 
file; null for neither. */
     private String uriCompletion;
     /** The completion being chosen in a uri: its row, the column its prefix 
ends at, the prefix and suffix. */
@@ -406,6 +407,11 @@ class SourceViewer {
     private record UriCompletion(int row, int endCol, String prefix, String 
suffix) {
     }
 
+    /** Opens the AI panel to fix the problem of the cursor line (Shift+F8); 
null when there is no AI panel. */
+    void setAskAi(MonitorContext.AskAi askAi) {
+        this.askAi = askAi;
+    }
+
     void hide() {
         exitEditMode();
         visible = false;
@@ -870,6 +876,10 @@ class SourceViewer {
             }
             return true;
         }
+        if (ke.isKey(KeyCode.F8) && ke.hasShift()) {
+            askAiToFix();
+            return true;
+        }
         if (ke.isKey(KeyCode.F9) && ke.hasShift()) {
             // Shift+F9 fixes the problem F9 jumps to (F3, F6, F8 are global: 
switch integration, shell, AI panel)
             applyQuickFix();
@@ -1656,6 +1666,34 @@ class SourceViewer {
         return error != null ? QuickFixes.fixFor(error, 
editState.getLine(row)) : null;
     }
 
+    /**
+     * 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.
+     */
+    private void askAiToFix() {
+        int row = editState.cursorRow();
+        String problem = visibleInlineErrors().get(row);
+        if (askAi == null || editableFile == null || problem == null) {
+            return;
+        }
+        String lineText = editState.getLine(row);
+        Path file = editableFile;
+        if (dirty) {
+            try {
+                Files.writeString(file, editState.text(), 
StandardCharsets.UTF_8);
+            } catch (IOException e) {
+                notifySave("Save failed: " + e.getMessage(), true);
+                return;
+            }
+            dirty = false;
+        }
+        exitEditMode();
+        loadFile(file);
+        goToLine(row);
+        askAi.fixProblem(file, row + 1, problem, lineText);
+    }
+
     /** Applies the fix of the problem on the line of the cursor (Shift+F9), 
and checks the content again right away. */
     private void applyQuickFix() {
         QuickFixes.Fix fix = cursorFix();
@@ -2313,10 +2351,17 @@ class SourceViewer {
                         Span.styled(suffix + "─".repeat(remaining), 
errorDim)));
                 docLines.add(Line.from(Span.styled(cursorError, errorDim)));
                 QuickFixes.Fix fix = cursorFix();
+                List<Span> actions = new ArrayList<>();
                 if (fix != null) {
-                    docLines.add(Line.from(
-                            Span.styled("Shift+F9", Style.EMPTY.bold()),
-                            Span.styled(" fix: " + fix.label(), 
Style.EMPTY.dim())));
+                    actions.add(Span.styled("Shift+F9", Style.EMPTY.bold()));
+                    actions.add(Span.styled(" fix: " + fix.label() + "   ", 
Style.EMPTY.dim()));
+                }
+                if (askAi != null) {
+                    actions.add(Span.styled("Shift+F8", Style.EMPTY.bold()));
+                    actions.add(Span.styled(" fix with AI", 
Style.EMPTY.dim()));
+                }
+                if (!actions.isEmpty()) {
+                    docLines.add(Line.from(actions));
                 }
             } else if (titleText != null) {
                 String prefix = "─── ";
@@ -2520,6 +2565,9 @@ class SourceViewer {
             if (cursorFix() != null) {
                 TuiHelper.hint(spans, "Shift+F9", "fix");
             }
+            if (askAi != null && 
visibleInlineErrors().containsKey(editState.cursorRow())) {
+                TuiHelper.hint(spans, "Shift+F8", "fix with AI");
+            }
             if (isCamelYamlFile()) {
                 TuiHelper.hint(spans, "Ctrl+R", "refactor");
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 2b3834edded9..18a054497b65 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -54,6 +54,9 @@ a heat map of the route: where messages go, where they fail.
 - Quick documentation panel is shown at the bottom (shows doc for current line)
 - **F7** — show diff of unsaved changes
 - **F9** — jump to next validation error
+- **Shift+F8** — ask the AI to fix the problem on the cursor line: the file is
+  saved as it is, and the AI panel opens with the question in its input (file,
+  line, problem); press Enter to send it, or change it first
 - **Shift+F9** — apply the fix of the problem on the cursor line, when the 
problem
   says it (an option typo, an enum value a letter off, `to` that should be
   `toD`, `${key}` where `{{key}}` is meant); the Error panel shows the fix
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
index f47d9217eada..5e24312b342a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceQuickFixTest.java
@@ -46,6 +46,59 @@ class SourceQuickFixTest {
         Theme.resetForTesting();
     }
 
+    @Test
+    void theQuestionToFixAProblemWithAi() {
+        String q = AiFixPrompt.of(Path.of("/work/app"), 
Path.of("/work/app/src/MyRoute.java"), 6,
+                "timer: Unknown option 'peroid'", "        
from(\"timer:tick?peroid=1000\")");
+        assertThat(q).startsWith("Fix the problem on line 6 of 
src/MyRoute.java: timer: Unknown option 'peroid'\n")
+                .contains("The line is: from(\"timer:tick?peroid=1000\")")
+                .contains("camel_edit_file").contains("camel_validate_source");
+    }
+
+    @Test
+    void shiftF8SavesTheFileAndAsksTheAiToFixTheProblem() throws Exception {
+        Path file = tempDir.resolve("MyRoute.java");
+        Files.writeString(file, """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class MyRoute extends RouteBuilder {
+                    @Override
+                    public void configure() throws Exception {
+                        from("timer:tick?period=1000")
+                            .to("seda:out");
+                    }
+                }
+                """, StandardCharsets.UTF_8);
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of());
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        SourceEditAssist assist = new SourceEditAssist(new 
MonitorContext(data, infraData));
+        SourceViewer viewer = new SourceViewer();
+        viewer.setRouteValidator(content -> assist.validateRoutes(file, 
content));
+        AtomicReference<String> asked = new AtomicReference<>();
+        viewer.setAskAi((f, line, problem, text) -> asked.set(f.getFileName() 
+ ":" + line + ": " + problem));
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+        // an unsaved edit that makes a problem on line 6: period becomes perio
+        for (int i = 0; i < 5; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.HOME, KeyModifiers.NONE));
+        int col = viewer.editText().split("\n")[5].indexOf("period") + 6;
+        for (int i = 0; i < col; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.RIGHT, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.BACKSPACE, 
KeyModifiers.NONE));
+        // the problem is marked (Ctrl+S would too; the AI fix needs the 
problem on the line)
+        viewer.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.CTRL));
+        assertThat(viewer.inlineErrors()).containsKey(5);
+
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F8, KeyModifiers.SHIFT));
+        assertThat(asked.get()).startsWith("MyRoute.java:6: timer: Unknown 
option 'perio'");
+        // saved as it was in the editor, and the editor shows the file the AI 
changes
+        assertThat(Files.readString(file, 
StandardCharsets.UTF_8)).contains("perio=1000");
+        assertThat(viewer.isEditMode()).isFalse();
+    }
+
     @Test
     void shiftF9AppliesTheFixOfTheProblemOnTheLine() throws Exception {
         Path file = tempDir.resolve("MyRoute.java");

Reply via email to