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");