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 131ec37bcbf8 CAMEL-25411: camel-jbang - Source editor: Kamelet 
property completion, highlighting, Go to Route names and notes as warnings 
(#27487)
131ec37bcbf8 is described below

commit 131ec37bcbf8b810f9b1785e33fd7b04ba11e7b5
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 7 13:05:18 2026 +0200

    CAMEL-25411: camel-jbang - Source editor: Kamelet property completion, 
highlighting, Go to Route names and notes as warnings (#27487)
    
    * CAMEL-25411: camel-jbang - Source editor: complete the properties of a 
Kamelet under its kamelet: endpoint
    
    Tab under parameters: (or in the query) of a kamelet: endpoint listed only
    the options of the kamelet component itself (routeId, timeout...). It now
    lists the properties of the Kamelet first, from the project's own Kamelet
    file or the catalog, required first, with their type, default and
    description, then a dim divider and the options of the kamelet component,
    which are for special uses. There is no divider when only one kind is
    listed, as for a Kamelet without properties.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: camel-jbang - Source editor: name the options popup of a 
kamelet: endpoint after its Kamelet
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: camel-jbang - Source editor: Kamelet files are highlighted, 
named in Go to Route, and their notes shown as warnings
    
    - A name.kamelet.yaml file was shown as plain text: the language is now
      picked by the last extension.
    - The template of a Kamelet was listed in Go to Route as kamelet:source,
      the same for every Kamelet: it is now named after metadata.name and
      reached as kamelet:<name>, so a route's to: kamelet:<name> links to it.
    - A note (an unused dependency, camel:core) was marked and counted as an
      error: it is now a warning mark, counted apart as notes, and its panel
      says Note.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: camel-jbang - An unused Kamelet dependency says to remove 
its line, and Shift+F9 removes it
    
    Told only what a Kamelet lists, a local model replaced the unused
    camel:timer with kamelet:source. The note now says to remove the line, and
    the fix is certain: Shift+F9 in the source editor removes it, and
    camel_validate_source returns it as a find/replace. When it was the last
    dependency, the dependencies: key goes with it.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: camel-jbang - Review: drop the unreachable multi-dot 
extensions, match kamelet:source exactly
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: camel-jbang - Source editor: a route sent to from several 
places links to a popup of its callers
    
    The from: line of a route linked back to the first route that sends to it
    only. A Kamelet, or a direct: route, used by more than one route now links
    as "2 callers", and Enter opens a popup of the callers to choose one. With
    one caller the link jumps to it as before.
    
    Also the review: the scrollbar of the completion popup counts the divider.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: camel-jbang - Source editor: the usages popup does not list 
the line it is opened on
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: docs - source editor: Kamelets, notes as warnings, the 
callers of a route
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25411: docs - source editor: a screenshot of the completion of a 
Kamelet's properties
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang/camel-tui-source-kamelet-completion.png  | Bin 0 -> 310326 bytes
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  35 +++++-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  17 ++-
 .../dsl/jbang/core/commands/ai/KameletChecks.java  |   7 +-
 .../dsl/jbang/core/commands/ai/QuickFixes.java     |  53 ++++++++-
 .../jbang/core/commands/ai/KameletChecksTest.java  |  31 ++++++
 .../jbang/core/commands/tui/AutocompletePopup.java |  63 +++++++++--
 .../jbang/core/commands/tui/SourceEditAssist.java  |  47 ++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 116 ++++++++++++++++---
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 123 ++++++++++++++++++---
 .../jbang/core/commands/tui/SyntaxHighlighter.java |   9 +-
 .../commands/tui/SourceKameletValidationTest.java  | 100 +++++++++++++++++
 .../core/commands/tui/SourceTabJumpLinksTest.java  |  60 +++++++++-
 .../core/commands/tui/SyntaxHighlighterTest.java   |   8 ++
 14 files changed, 608 insertions(+), 61 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-kamelet-completion.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-kamelet-completion.png
new file mode 100644
index 000000000000..157041b7c32d
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-kamelet-completion.png
 differ
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 c4787850c838..aced09a79f96 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
@@ -22,6 +22,8 @@ routes.
   with *Ctrl+G*, see where the cursor is in the route.
 * <<_changes_and_diff,*Changes and diff*>> -- changed lines are marked, and 
*F7* shows the unsaved changes as a diff.
 * <<_yaml_java_and_xml,*YAML, Java and XML*>> -- *Space* shows a route in any 
of the three DSLs.
+* <<_kamelets,*Kamelets*>> -- write your own Kamelets with the same checks, 
and *Tab* completes the properties of a
+  Kamelet where a route uses it.
 
 == Browsing and Editing
 
@@ -64,7 +66,9 @@ the title. The panel at the bottom says the problem of the 
selected line, and *F
 image::jbang/camel-tui-source-problems-on-open.png[The problem of a YAML route 
marked when the file opens]
 
 In the editor, a line with a problem has a red line number, the title shows 
how many errors there are, and the Error
-panel at the bottom says what is wrong with the line the cursor is on. They 
are checked again as you type, and *F9*
+panel at the bottom says what is wrong with the line the cursor is on. A note, 
something that works but is not right
+(such as a dependency a Kamelet does not use), is marked as a warning instead: 
a yellow `⚠`, an amber line number, and
+counted apart in the title as `notes`. A note does not keep the file from 
being saved. They are checked again as you type, and *F9*
 jumps to the next problem.
 
 image::jbang/camel-tui-source-problems.png[A misspelled seda option marked 
while typing]
@@ -85,7 +89,7 @@ The checks can be turned off in the Settings (*F2* > 
Settings).
 
 Many problems say how to fix them: an option typo (`siz` instead of `size`), 
an enum value a letter off, a `to` that
 should be a `toD`, `+${key}+` where the property placeholder `+{{key}}+` is 
meant, or a Simple function the error names
-the right one of (`+${bdy}+` for `+${body}+`). For these, the Error panel 
shows the fix, and *Shift+F9* applies it to the
+the right one of (`+${bdy}+` for `+${body}+`), or a dependency a Kamelet does 
not use, whose line is removed. For these, the Error panel shows the fix, and 
*Shift+F9* applies it to the
 line:
 
 image::jbang/camel-tui-source-quick-fix.png[Shift+F9 fixed the option typo]
@@ -235,11 +239,11 @@ image::jbang/camel-tui-source-xml-quick-doc.png[The 
documentation of the logging
 
 * *Jump links* -- a line that sends to another route, such as 
`to("seda:shipping")`, shows `↵ shipping`. Press *Enter*
   on the line to go to that route, also when it is in another file or another 
DSL. A `from` shows which route calls
-  it.
+  it; when more than one does, it shows `↵ 2 callers`, and *Enter* opens a 
list of them to choose from.
 * *Go to route* -- *g* lists all the routes of the project; type to filter, 
and *Enter* opens the route.
 * *Bean jumps* -- a line that refers to a bean shows `↵ name`, and *Enter* 
goes to where the project declares it.
 * *Usages* -- *u* on a line with an endpoint lists where it is used: the 
routes that consume from it and the steps that
-  send to it, across the project and its DSLs. *Enter* goes there.
+  send to it, across the project and its DSLs, without the line you are on. 
*Enter* goes there.
 * *Go to node* -- *Ctrl+G* shows the routes of the YAML, Java and XML files 
and their processors as a tree (the `when`
   and `otherwise` of a choice included). Type to filter by route ID, EIP or 
label, or type a line number, and *Enter*
   jumps there.
@@ -281,6 +285,29 @@ something it cannot say (a lambda processor, a predicate 
built in Java code) is
 
 image::jbang/camel-tui-source-convert.png[An XML route converted to YAML, with 
the note of what did not carry over]
 
+== Kamelets
+
+A Kamelet file (`name.kamelet.yaml`) in the project opens and edits like a 
route file, with syntax highlighting and the
+same checks, and more that are particular to Kamelets:
+
+* *The shape of the file* -- the properties go under `spec.definition` (with 
the required ones listed in
+  `spec.definition.required`), and the template of an action or a sink starts 
`from: kamelet:source`, never from the
+  Kamelet itself.
+* *Where a route uses it* -- a `kamelet:` endpoint is checked against the 
properties of its Kamelet: a misspelled one
+  (`tagg` for `tag`) and a required one left out are marked. The project's own 
Kamelet files are read, so this works for
+  the Kamelets you write as well as for the ones of the catalog.
+* *Dependencies* -- a `camel:` dependency the template does not use, and 
`camel:core`, which every Camel runtime has, are
+  marked as notes. *Shift+F9* removes the line (and the `dependencies:` key 
when it was the last one).
+
+*Tab* under the `parameters:` of a `kamelet:` endpoint lists the properties of 
the Kamelet first, the required ones on
+top, with their type, default and description. Below a divider come the 
options of the kamelet component itself
+(`routeId`, `timeout`...), which are for special uses only.
+
+image::jbang/camel-tui-source-kamelet-completion.png[Tab under the parameters 
of a kamelet: endpoint lists the properties of the Kamelet first]
+
+The template of a Kamelet is a route of the project: *g* lists it under the 
name of the Kamelet, a route's
+`to: kamelet:name` has a jump link to it, and the `from` of the template links 
back to the routes that use it.
+
 == Keyboard Shortcuts
 
 === Viewing
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index dd6fb1a460c0..6a51293e425e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -535,13 +535,26 @@ public final class AuthoringTools {
         // the problems whose fix is certain, as edits an agent can apply 
(camel_edit_file find/replace)
         JsonArray fixes = new JsonArray();
         String[] lines = content.split("\n", -1);
-        for (String error : errors) {
+        List<String> problems = new ArrayList<>(errors);
+        if (result.get("notes") instanceof JsonArray notes) {
+            for (Object note : notes) {
+                problems.add(String.valueOf(note));
+            }
+        }
+        for (String error : problems) {
             int line = lineOf(error);
             QuickFixes.Fix fix = line > 0 && line <= lines.length ? 
QuickFixes.fixFor(error, lines[line - 1]) : null;
             if (fix != null) {
                 JsonObject jo = new JsonObject();
                 jo.put("line", line);
-                jo.put("find", fix.oldText());
+                String find = fix.oldText();
+                if (fix.removesLine()) {
+                    // the line goes with its line break, and the key above it 
when it was its only item
+                    int[] range = QuickFixes.linesToRemove(List.of(lines), 
line - 1);
+                    find = String.join("\n", List.of(lines).subList(range[0], 
range[1] + 1))
+                           + (range[1] + 1 < lines.length ? "\n" : "");
+                }
+                jo.put("find", find);
                 jo.put("replace", fix.newText());
                 jo.put("fix", fix.label());
                 fixes.add(jo);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
index e0928e5de459..7ad2b6622839 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
@@ -421,7 +421,7 @@ public final class KameletChecks {
             if ("core".equals(name)) {
                 // every Camel runtime has camel-core; camel:kamelet is not 
implied, it runs the template
                 notes.add(EndpointChecks.linePrefix(line(d)) + 
"spec.dependencies: camel:core is implied, every Camel"
-                          + " runtime has it: leave it out");
+                          + " runtime has it: remove this line");
                 continue;
             }
             if (!inspectable) {
@@ -433,9 +433,10 @@ public final class KameletChecks {
                     || usedArtifacts.contains(model.getArtifactId()) || 
delegatedTo(used, name)) {
                 continue;
             }
+            // what to do first: told only what to list, a model put another 
dependency on the line
             notes.add(EndpointChecks.linePrefix(line(d)) + "spec.dependencies: 
" + dep + " is not used by the template"
-                      + " (it has no " + name + ": endpoint): list only the 
components, languages and data formats the"
-                      + " template uses");
+                      + " (it has no " + name + ": endpoint): remove this 
line; a Kamelet lists only the components,"
+                      + " languages and data formats its template uses");
         }
         return notes;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
index 2c8d330cde10..9b3e0ab476f0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
@@ -40,12 +40,22 @@ public final class QuickFixes {
      * @param oldText the text on the line to replace (its first occurrence)
      * @param newText what it is replaced with
      */
-    public record Fix(String label, String oldText, String newText) {
+    public record Fix(String label, String oldText, String newText, boolean 
removesLine) {
 
-        /** The line with the fix applied, or null when the line does not have 
the text to replace. */
+        public Fix(String label, String oldText, String newText) {
+            this(label, oldText, newText, false);
+        }
+
+        /**
+         * The line with the fix applied, or null when the line does not have 
the text to replace. A fix that removes
+         * the line gives an empty line: the caller removes it.
+         */
         public String apply(String line) {
             int i = line != null ? line.indexOf(oldText) : -1;
-            return i < 0 ? null : line.substring(0, i) + newText + 
line.substring(i + oldText.length());
+            if (i < 0) {
+                return null;
+            }
+            return removesLine ? "" : line.substring(0, i) + newText + 
line.substring(i + oldText.length());
         }
     }
 
@@ -58,6 +68,9 @@ public final class QuickFixes {
     private static final Pattern INVALID_BOOLEAN = Pattern.compile("Invalid 
boolean value '([^']*)' for option '([^']+)'");
     private static final Pattern SIMPLE_AS_PLACEHOLDER = Pattern.compile(
             "([\\w.-]+)=(\\$\\{[^}]*\\}) is a Simple expression, which an 
endpoint option is not evaluated as.*\\{\\{([^}]+)\\}\\}");
+    // the notes of a Kamelet file (CAMEL-25403): the dependency goes, with 
its line
+    private static final Pattern UNUSED_DEPENDENCY
+            = Pattern.compile("spec\\.dependencies: (camel:[\\w.-]+) (?:is not 
used by the template|is implied)");
     private static final Pattern DYNAMIC_TO = Pattern.compile("holds an 
expression \\(\\$\\{");
     private static final Pattern UNKNOWN_FUNCTION = Pattern.compile("Unknown 
function: (.+?) \\((?:the argument goes in"
                                                                     + " 
parentheses: )?(?:did you mean |function names are"
@@ -66,6 +79,34 @@ public final class QuickFixes {
     private QuickFixes() {
     }
 
+    /**
+     * The lines, from 0, a fix that removes the line of the row removes: the 
row, and the key above it when the row is
+     * its only item, so that the last dependency of a Kamelet does not leave 
an empty dependencies: behind.
+     *
+     * @return the first and the last line to remove
+     */
+    public static int[] linesToRemove(List<String> lines, int row) {
+        int first = row;
+        if (row > 0 && lines.get(row).trim().startsWith("- ") && lines.get(row 
- 1).trim().endsWith(":")
+                && !lines.get(row - 1).trim().startsWith("- ")) {
+            int itemIndent = indent(lines.get(row));
+            boolean more = row + 1 < lines.size() && !lines.get(row + 
1).isBlank()
+                    && indent(lines.get(row + 1)) >= itemIndent && 
lines.get(row + 1).trim().startsWith("- ");
+            if (!more) {
+                first = row - 1;
+            }
+        }
+        return new int[] { first, row };
+    }
+
+    private static int indent(String line) {
+        int i = 0;
+        while (i < line.length() && line.charAt(i) == ' ') {
+            i++;
+        }
+        return i;
+    }
+
     /**
      * The fix of a problem.
      *
@@ -82,7 +123,11 @@ public final class QuickFixes {
     }
 
     private static Fix find(String message, String line) {
-        Matcher m = UNKNOWN_OPTION.matcher(message);
+        Matcher m = UNUSED_DEPENDENCY.matcher(message);
+        if (m.find()) {
+            return line.contains(m.group(1)) ? new Fix("remove " + m.group(1), 
line, "", true) : null;
+        }
+        m = UNKNOWN_OPTION.matcher(message);
         if (m.find()) {
             return rename(line, m.group(1), m.group(2).trim());
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
index c3b3f142073a..821a8627909b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
@@ -320,6 +320,37 @@ class KameletChecksTest {
                 .contains("camel:core is implied");
     }
 
+    @Test
+    void anUnusedDependencyIsFixedByRemovingItsLine() {
+        // told only what to list, a local model replaced camel:timer with 
kamelet:source: the note says to remove it,
+        // and the fix removes the line
+        String content = withDependencies(TAG_KAMELET.formatted("${body} 
[{{tag}}]"), "camel:kamelet", "camel:timer");
+        String note = KameletChecks.unusedDependencies(content).get(0);
+        assertThat(note).contains("camel:timer is not used by the 
template").contains("remove this line");
+        QuickFixes.Fix fix = QuickFixes.fixFor(note, "    - \"camel:timer\"");
+        assertThat(fix).isNotNull();
+        assertThat(fix.removesLine()).isTrue();
+        assertThat(fix.label()).isEqualTo("remove camel:timer");
+        assertThat(QuickFixes.fixFor("spec.dependencies: camel:core is 
implied, every Camel runtime has it: remove this line",
+                "    - camel:core").removesLine()).isTrue();
+        // the fix as camel_validate_source gives it: the line and its line 
break go
+        JsonObject result = AuthoringTools.validate(new ToolContext(), 
"tag-order-action.kamelet.yaml", content);
+        assertThat(result.toJson()).contains("\"find\":\"    - 
\\\"camel:timer\\\"\\n\"").contains("\"replace\":\"\"");
+    }
+
+    @Test
+    void theLastDependencyGoesWithTheDependenciesKey() {
+        List<String> lines = List.of("spec:", "  dependencies:", "    - 
\"camel:timer\"", "  template:");
+        assertThat(QuickFixes.linesToRemove(lines, 2)).containsExactly(1, 2);
+        List<String> two = List.of("spec:", "  dependencies:", "    - 
\"camel:timer\"", "    - \"camel:kamelet\"");
+        assertThat(QuickFixes.linesToRemove(two, 2)).containsExactly(2, 2);
+        assertThat(QuickFixes.linesToRemove(two, 3)).containsExactly(3, 3);
+        // the model's Kamelet, whose only dependency was camel:timer
+        String content = withDependencies(TAG_KAMELET.formatted("${body} 
[{{tag}}]"), "camel:timer");
+        JsonObject result = AuthoringTools.validate(new ToolContext(), 
"tag-order-action.kamelet.yaml", content);
+        assertThat(result.toJson()).contains("\"find\":\"  dependencies:\\n    
- \\\"camel:timer\\\"\\n\"");
+    }
+
     @Test
     void theDependenciesTheTemplateUsesAreNotNoted() {
         String source = """
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
index 245bcb6fcde6..4552ee285820 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
@@ -98,6 +98,12 @@ class AutocompletePopup {
     private Rect popupRect;
     private String titlePrefix;
     private boolean fullKeys;
+    // the row of the filtered list a divider is drawn above, -1 for none: the 
properties of a Kamelet above it, the
+    // options of the kamelet component itself, for special uses only, below 
it (CAMEL-25411)
+    private int dividerAt = -1;
+
+    static final String KAMELET_GROUP = "kamelet ";
+    static final String KAMELET_COMPONENT_OPTIONS = "kamelet component 
options";
 
     AutocompletePopup(List<CompletionItem> items, String initialPrefix, String 
lineKeyText) {
         this(items, initialPrefix, lineKeyText, false);
@@ -224,11 +230,16 @@ class AutocompletePopup {
         if (me.isClick()) {
             if (popupRect != null && popupRect.contains(me.x(), me.y())) {
                 int idx = TuiHelper.listItemAt(popupRect, 0,
-                        (filteredItems != null ? filteredItems.size() : 0) + 2,
+                        (filteredItems != null ? listRows() : 0) + 2,
                         me.x(), me.y());
-                if (idx >= 2 && filteredItems != null && idx - 2 < 
filteredItems.size()) {
-                    listState.select(idx - 2);
-                    selectedItem = filteredItems.get(idx - 2);
+                int row = idx - 2;
+                if (dividerAt >= 0 && row >= dividerAt) {
+                    // the divider is not an item
+                    row = row == dividerAt ? -1 : row - 1;
+                }
+                if (row >= 0 && filteredItems != null && row < 
filteredItems.size()) {
+                    listState.select(row);
+                    selectedItem = filteredItems.get(row);
                     return Result.CLOSED;
                 }
                 return Result.CONSUMED;
@@ -244,7 +255,7 @@ class AutocompletePopup {
         }
 
         int popupW = Math.max(70, area.width() - 4);
-        int contentH = filteredItems.size() + 2;
+        int contentH = listRows() + 2;
         int maxH = area.height() - 2;
         int popupH = Math.min(contentH + 2, maxH);
         popupH = Math.max(popupH, 12);
@@ -314,7 +325,13 @@ class AutocompletePopup {
         Style dimStyle = Style.EMPTY.dim();
         Style deprecatedStyle = Style.EMPTY.dim().crossedOut();
 
-        for (CompletionItem ci : filteredItems) {
+        for (int row = 0; row < filteredItems.size(); row++) {
+            CompletionItem ci = filteredItems.get(row);
+            if (row == dividerAt) {
+                String text = "── " + KAMELET_COMPONENT_OPTIONS + " ";
+                items.add(ListItem.from(Line.from(Span.styled(
+                        text + "─".repeat(Math.max(1, listRect.width() - 2 - 
text.length())), dimStyle))));
+            }
             List<Span> spans = new ArrayList<>();
 
             if (ci.deprecated()) {
@@ -362,7 +379,7 @@ class AutocompletePopup {
         ListState renderState = new ListState();
         Integer sel = listState.selected();
         if (sel != null) {
-            renderState.select(sel + 2);
+            renderState.select(sel + 2 + (dividerAt >= 0 && sel >= dividerAt ? 
1 : 0));
         }
 
         int total = allItems.size();
@@ -385,11 +402,11 @@ class AutocompletePopup {
         frame.renderStatefulWidget(list, listRect, renderState);
 
         int visibleRows = Math.max(1, listRect.height() - 2);
-        if (shown + 2 > visibleRows) {
+        if (listRows() + 2 > visibleRows) {
             scrollbarState
-                    .contentLength(shown)
+                    .contentLength(listRows())
                     .viewportContentLength(visibleRows)
-                    .position(sel != null ? sel : 0);
+                    .position(sel != null ? sel + (dividerAt >= 0 && sel >= 
dividerAt ? 1 : 0) : 0);
             frame.renderStatefulWidget(Scrollbar.builder().build(), listRect, 
scrollbarState);
         }
     }
@@ -565,9 +582,35 @@ class AutocompletePopup {
             // what was typed exactly first, then what starts with it: seda 
before hazelcast-seda
             filteredItems.sort(Comparator.comparingInt(item -> 
rank(item.key(), f)));
         }
+        dividerAt = -1;
+        boolean kamelet = 
filteredItems.stream().anyMatch(AutocompletePopup::isKameletProperty);
+        if (kamelet && 
!filteredItems.stream().allMatch(AutocompletePopup::isKameletProperty)) {
+            // the properties of the Kamelet stay above the divider while 
filtering (the sort is stable)
+            filteredItems.sort(Comparator.comparing(item -> 
!isKameletProperty(item)));
+            for (int i = 0; i < filteredItems.size(); i++) {
+                if (!isKameletProperty(filteredItems.get(i))) {
+                    dividerAt = i;
+                    break;
+                }
+            }
+        }
         listState.select(filteredItems.isEmpty() ? null : 0);
     }
 
+    private static boolean isKameletProperty(CompletionItem item) {
+        return item.group() != null && item.group().startsWith(KAMELET_GROUP);
+    }
+
+    /** Package-private for tests: the row of the filtered list the divider is 
drawn above, -1 for none. */
+    int dividerAt() {
+        return dividerAt;
+    }
+
+    /** The rows of the list: the items and the divider. */
+    private int listRows() {
+        return filteredItems.size() + (dividerAt >= 0 ? 1 : 0);
+    }
+
     private static int rank(String key, String filter) {
         if (key.equalsIgnoreCase(filter)) {
             return 0;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
index 3a9c306f10d3..2c4faf30563b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
@@ -40,6 +40,7 @@ import java.util.regex.Pattern;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.KameletChecks;
+import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteNodes;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
@@ -928,6 +929,14 @@ final class SourceEditAssist {
     }
 
     List<AutocompletePopup.CompletionItem> provideYamlKeyCompletions(String 
context) {
+        return provideYamlKeyCompletions(context, null);
+    }
+
+    /**
+     * As {@link #provideYamlKeyCompletions(String)}, with the directory of 
the file: the options of a kamelet: endpoint
+     * start with the properties of its Kamelet, from the project's own 
Kamelet file or the catalog (CAMEL-25411).
+     */
+    List<AutocompletePopup.CompletionItem> provideYamlKeyCompletions(String 
context, Path directory) {
         if (context == null) {
             return List.of();
         }
@@ -994,6 +1003,9 @@ final class SourceEditAssist {
         }
 
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        if ("kamelet".equals(componentName)) {
+            items.addAll(kameletPropertyCompletions(uri, directory, 
existingKeys));
+        }
         for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
             if (!includeEndpointOption(opt, isConsumer)) {
                 continue;
@@ -1008,11 +1020,46 @@ final class SourceEditAssist {
         }
 
         
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                // the properties of the Kamelet before the options of the 
kamelet component
+                .thenComparing(i -> i.group() == null || 
!i.group().startsWith(AutocompletePopup.KAMELET_GROUP))
                 .thenComparing((a, b) -> Boolean.compare(b.required(), 
a.required()))
                 .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
         return items;
     }
 
+    /**
+     * The properties of the Kamelet a kamelet: uri names, the ones not set 
yet: what goes under parameters: beside the
+     * options of the kamelet component itself (routeId, timeout...), which 
are all a model or a user saw before.
+     */
+    static List<AutocompletePopup.CompletionItem> kameletPropertyCompletions(
+            String uri, Path directory, Set<String> existingKeys) {
+        if (uri == null || !uri.startsWith("kamelet:")) {
+            return List.of();
+        }
+        String name = uri.substring("kamelet:".length());
+        int end = name.length();
+        for (char c : new char[] { '?', '/' }) {
+            int i = name.indexOf(c);
+            if (i >= 0) {
+                end = Math.min(end, i);
+            }
+        }
+        KameletDefinitions.Definition def = 
KameletDefinitions.find(name.substring(0, end).trim(), directory);
+        if (def == null) {
+            return List.of();
+        }
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (KameletDefinitions.Property p : def.properties()) {
+            if (existingKeys.contains(p.name())) {
+                continue;
+            }
+            items.add(new AutocompletePopup.CompletionItem(
+                    p.name(), p.description(), p.type() != null ? p.type() : 
"string", p.defaultValue(), false, null,
+                    AutocompletePopup.KAMELET_GROUP + def.name(), 
p.required()));
+        }
+        return items;
+    }
+
     /** The quick doc of the XML element or attribute at the cursor 
(CAMEL-25244). */
     List<SourceViewer.DocEntry> provideXmlQuickDoc(List<String> lines, int 
row, int col) {
         return XmlQuickDoc.at(validationCatalog(), lines, row, col);
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 966f04171377..5e9f1d98d82f 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
@@ -907,7 +907,9 @@ class SourceTab extends AbstractTab {
             sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
             sourceViewer.setDeprecatedLineScanner(null);
             if (SourceEditAssist.isYamlFile(filePath)) {
-                
sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
+                // the options of a kamelet: endpoint start with the 
properties of its Kamelet (CAMEL-25411)
+                sourceViewer.setAutocompleteProvider(
+                        c -> assist.provideYamlKeyCompletions(c, 
filePath.toAbsolutePath().getParent()));
                 
sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
                 
sourceViewer.setEndpointValidator(assist::validateYamlEndpoints);
                 // in a Kamelet's template, a property written as 
${header.tag} is said to be the placeholder {{tag}}
@@ -1388,6 +1390,8 @@ class SourceTab extends AbstractTab {
         }
 
         String filePath = file.toString();
+        // the template of a Kamelet is the route of kamelet:<its name>, named 
after it (CAMEL-25411)
+        String kamelet = SourceEditAssist.isKameletFile(file) ? 
kameletName(lines) : null;
         String currentRouteId = null;
         int routeIdIndent = -1;
         int pendingFromLine = -1;
@@ -1425,7 +1429,8 @@ class SourceTab extends AbstractTab {
                 linkableBlockIndent = -1;
                 String inlineUri = extractInlineUri(trimmed, "from");
                 if (inlineUri != null) {
-                    emitRouteEntry(fromEntries, currentRouteId, inlineUri, 
filePath, i);
+                    emitRouteEntry(fromEntries, kameletRouteId(kamelet, 
currentRouteId), kameletFromUri(kamelet, inlineUri),
+                            filePath, i);
                 } else {
                     pendingFromLine = i;
                 }
@@ -1436,7 +1441,8 @@ class SourceTab extends AbstractTab {
             if (pendingFromLine >= 0 && trimmed.startsWith("uri:")) {
                 String uri = extractYamlValue(trimmed, "uri");
                 if (uri != null) {
-                    emitRouteEntry(fromEntries, currentRouteId, uri, filePath, 
pendingFromLine);
+                    emitRouteEntry(fromEntries, kameletRouteId(kamelet, 
currentRouteId), kameletFromUri(kamelet, uri),
+                            filePath, pendingFromLine);
                 }
                 pendingFromLine = -1;
                 continue;
@@ -1489,6 +1495,33 @@ class SourceTab extends AbstractTab {
         }
     }
 
+    /** The metadata.name of a Kamelet file, or null. */
+    static String kameletName(List<String> lines) {
+        boolean metadata = false;
+        for (String line : lines) {
+            if (line.isBlank() || line.trim().startsWith("#")) {
+                continue;
+            }
+            if (lineIndent(line) == 0) {
+                metadata = line.trim().equals("metadata:");
+            } else if (metadata && line.trim().startsWith("name:")) {
+                String name = extractYamlValue(line.trim(), "name");
+                return name != null && !name.isEmpty() ? name : null;
+            }
+        }
+        return null;
+    }
+
+    private static String kameletRouteId(String kamelet, String routeId) {
+        return kamelet != null && (routeId == null || routeId.isEmpty()) ? 
kamelet : routeId;
+    }
+
+    /** An action or a sink is entered from kamelet:source: the routes reach 
it as kamelet:<its name>. */
+    private static String kameletFromUri(String kamelet, String uri) {
+        return kamelet != null && (uri.equals("kamelet:source") || 
uri.startsWith("kamelet:source?"))
+                ? "kamelet:" + kamelet : uri;
+    }
+
     private void emitRouteEntry(List<RouteEntry> index, String routeId, String 
fromUri, String filePath, int fromLine) {
         String baseUri = stripQueryParams(fromUri);
         if (baseUri == null || baseUri.isEmpty()) {
@@ -1685,23 +1718,23 @@ class SourceTab extends AbstractTab {
         return result;
     }
 
-    /** Reverse links: a from line to a route that sends to it (jumps to the 
caller's to line). */
+    /**
+     * Reverse links: a from line to the route that sends to it (jumps to the 
caller's to line), or when several do, to
+     * a popup to choose one of them: a Kamelet or a direct: route used by 
more than one route.
+     */
     private void addReverseLinks(String currentFilePath, Map<Integer, 
SourceViewer.JumpLink> result) {
         for (RouteEntry re : routeIndex) {
             if (!currentFilePath.equals(re.filePath())) {
                 continue;
             }
-            for (ToEntry te : toIndex) {
-                if (te.routeId().equals(re.routeId())) {
-                    continue;
-                }
-                if (re.fromUri().equals(te.toUri())) {
-                    // add jump link on the from: line pointing to the caller
-                    String callerRouteId = te.routeId().isEmpty() ? "route" : 
te.routeId();
-                    result.putIfAbsent(re.fromLine(),
-                            new SourceViewer.JumpLink(callerRouteId, 
te.filePath(), te.toLine()));
-                    break;
-                }
+            List<ToEntry> callers = callers(re);
+            if (callers.size() == 1) {
+                ToEntry te = callers.get(0);
+                String callerRouteId = te.routeId().isEmpty() ? "route" : 
te.routeId();
+                result.putIfAbsent(re.fromLine(), new 
SourceViewer.JumpLink(callerRouteId, te.filePath(), te.toLine()));
+            } else if (callers.size() > 1) {
+                result.putIfAbsent(re.fromLine(),
+                        new SourceViewer.JumpLink(callers.size() + " callers", 
null, -1, re.fromUri()));
             }
         }
     }
@@ -1741,7 +1774,27 @@ class SourceTab extends AbstractTab {
             }
             return;
         }
-        gotoRoutePopup.openItems(usagesOf(uri), "Usages of " + uri);
+        // the line the popup is opened on is not one of the places to go to
+        List<GotoRoutePopup.RouteItem> items = withoutLine(usagesOf(uri),
+                sourceViewer.getCurrentFilePath(), 
sourceViewer.getSelectedLine());
+        if (items.isEmpty()) {
+            if (ctx.notificationCallback != null) {
+                ctx.notificationCallback.accept(uri + " is used only here", 
false);
+            }
+            return;
+        }
+        gotoRoutePopup.openItems(items, "Usages of " + uri);
+    }
+
+    /** The places without the one at the line of the file. */
+    static List<GotoRoutePopup.RouteItem> 
withoutLine(List<GotoRoutePopup.RouteItem> items, String file, int line) {
+        List<GotoRoutePopup.RouteItem> answer = new ArrayList<>();
+        for (GotoRoutePopup.RouteItem item : items) {
+            if (!(item.filePath().equals(file) && item.fromLine() == line)) {
+                answer.add(item);
+            }
+        }
+        return answer;
     }
 
     /** The routes that consume from the endpoint and the steps that send to 
it. */
@@ -1795,9 +1848,40 @@ class SourceTab extends AbstractTab {
     }
 
     private void handleJumpLink(SourceViewer.JumpLink link) {
+        if (link.callersOf() != null) {
+            // sent to from more than one place: choose which
+            gotoRoutePopup.openItems(callersOf(link.callersOf()), "Callers of 
" + link.callersOf());
+            return;
+        }
         openFileAt(link.filePath(), link.targetLine());
     }
 
+    /** The steps of other routes that send to the endpoint a route consumes 
from. */
+    List<GotoRoutePopup.RouteItem> callersOf(String uri) {
+        List<GotoRoutePopup.RouteItem> items = new ArrayList<>();
+        for (RouteEntry re : routeIndex) {
+            if (uri.equals(re.fromUri())) {
+                for (ToEntry te : callers(re)) {
+                    String routeId = te.routeId().isEmpty() ? "route" : 
te.routeId();
+                    items.add(new GotoRoutePopup.RouteItem(routeId, "to " + 
uri, te.filePath(), te.toLine()));
+                }
+                break;
+            }
+        }
+        return items;
+    }
+
+    /** The steps of the other routes that send to the route. */
+    private List<ToEntry> callers(RouteEntry re) {
+        List<ToEntry> answer = new ArrayList<>();
+        for (ToEntry te : toIndex) {
+            if (!te.routeId().equals(re.routeId()) && 
re.fromUri().equals(te.toUri())) {
+                answer.add(te);
+            }
+        }
+        return answer;
+    }
+
     /** A uri: line, also as the first key of a list item: - uri: 
direct:billing in the cases of a switch. */
     private static boolean isUriLine(String trimmed) {
         return trimmed.startsWith("uri:") || trimmed.startsWith("- uri:");
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 ffe17de3f105..403a12dd88af 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
@@ -23,6 +23,7 @@ import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -125,7 +126,15 @@ class SourceViewer {
         Set<Integer> scan(List<JsonObject> codeData);
     }
 
-    record JumpLink(String routeId, String filePath, int targetLine) {
+    /**
+     * A link on a line to where it leads; callersOf is the endpoint of a 
route sent to from more than one place, whose
+     * link opens a popup to choose one (CAMEL-25411), else null.
+     */
+    record JumpLink(String routeId, String filePath, int targetLine, String 
callersOf) {
+
+        JumpLink(String routeId, String filePath, int targetLine) {
+            this(routeId, filePath, targetLine, null);
+        }
     }
 
     /**
@@ -218,6 +227,8 @@ class SourceViewer {
     // the Kamelet checks of a YAML file (CAMEL-25411): its problems, and the 
notes that do not block a save
     private EndpointValidator kameletValidator;
     private EndpointValidator kameletNotes;
+    // the lines whose problem is a note, not an error: marked as a warning 
and counted apart (CAMEL-25411)
+    private Set<Integer> noteLines = Set.of();
     private EndpointValidator routeValidator;
     private LiveRunData liveRunData;
     private LineFailures lineFailures;
@@ -545,6 +556,7 @@ class SourceViewer {
         simpleValidator = null;
         kameletValidator = null;
         kameletNotes = null;
+        noteLines = Set.of();
         routeValidator = null;
         uriCompletion = null;
         simpleCompletion = null;
@@ -592,6 +604,7 @@ class SourceViewer {
         simpleValidator = null;
         kameletValidator = null;
         kameletNotes = null;
+        noteLines = Set.of();
         routeValidator = null;
         uriCompletion = null;
         simpleCompletion = null;
@@ -1828,7 +1841,7 @@ class SourceViewer {
                 List<AutocompletePopup.CompletionItem> items = 
autocompleteProvider.provide(context);
                 if (items != null && !items.isEmpty()) {
                     autocompletePopup = new AutocompletePopup(items, filter, 
filter);
-                    autocompletePopup.setTitlePrefix(ctx.component() + " 
options");
+                    
autocompletePopup.setTitlePrefix(optionsTitle(ctx.component(), ctx.uri()));
                 }
             }
             return;
@@ -2075,6 +2088,7 @@ class SourceViewer {
                 }
             }
             addAll(msgs, kameletValidator, content);
+            noteLines = Set.of();
             if (!msgs.isEmpty()) {
                 validationErrors = msgs;
                 validationErrorScroll = 0;
@@ -2087,6 +2101,7 @@ class SourceViewer {
                 // marked, and said when saved, but not blocking the save
                 routeProblems = notes;
                 inlineErrors = buildInlineErrors(notes, content);
+                noteLines = new HashSet<>(inlineErrors.keySet());
                 return;
             }
         } else if (validateOnSave && isPropertiesFile() && propertiesValidator 
!= null) {
@@ -2228,6 +2243,19 @@ class SourceViewer {
         if (fixed == null) {
             return;
         }
+        if (fix.removesLine()) {
+            // an unused dependency of a Kamelet: the line goes, as with 
Ctrl+K, and the dependencies: key above it
+            // when it was the last one
+            int[] range = QuickFixes.linesToRemove(editLines(), row);
+            List<String> kept = new ArrayList<>(editLines());
+            kept.subList(range[0], range[1] + 1).clear();
+            applyBlockEdit(new YamlBlockEditor.EditResult(kept, 
Math.min(range[0], Math.max(0, kept.size() - 1)), 0));
+            dirty = true;
+            lineStatuses = null;
+            lastBackgroundValidationTime = 0;
+            notifySave("Fixed: " + fix.label(), false);
+            return;
+        }
         recordEditChange();
         editState.moveCursorToLineStart();
         for (int i = 0; i < line.length(); i++) {
@@ -2288,6 +2316,7 @@ class SourceViewer {
     /** The problems the checks of the file type (Camel YAML, properties, Java 
or XML routes) find in the content. */
     private List<String> validateContent(String content) {
         List<String> msgs = new ArrayList<>();
+        noteLines = Set.of();
         if (isCamelYamlFile()) {
             if (endpointValidator != null) {
                 List<String> endpointErrors = 
endpointValidator.validate(content);
@@ -2302,7 +2331,17 @@ class SourceViewer {
                 }
             }
             addAll(msgs, kameletValidator, content);
-            addAll(msgs, kameletNotes, content);
+            List<String> notes = new ArrayList<>();
+            addAll(notes, kameletNotes, content);
+            if (!notes.isEmpty()) {
+                // a line with an error is marked as an error
+                Set<Integer> lines = new HashSet<>(buildInlineErrors(notes, 
content).keySet());
+                if (!msgs.isEmpty()) {
+                    lines.removeAll(buildInlineErrors(msgs, content).keySet());
+                }
+                noteLines = lines;
+                msgs.addAll(notes);
+            }
         } else if (isPropertiesFile() && propertiesValidator != null) {
             msgs.addAll(validateProperties(content));
         } else if (routeValidator != null) {
@@ -2314,6 +2353,44 @@ class SourceViewer {
         return msgs;
     }
 
+    /**
+     * The title of the options popup of an endpoint: the component, or for a 
kamelet: endpoint the Kamelet it names,
+     * whose properties the list starts with (CAMEL-25411).
+     */
+    static String optionsTitle(String component, String uri) {
+        if ("kamelet".equals(component) && uri != null && 
uri.startsWith("kamelet:")) {
+            String name = uri.substring("kamelet:".length()).split("[?/]", 
2)[0].trim();
+            if (!name.isEmpty()) {
+                return name + " options";
+            }
+        }
+        return component + " options";
+    }
+
+    /** The title spans counting the problems of the lines: the errors, and 
apart from them the notes. */
+    private List<Span> problemCounts(Map<Integer, String> problems, Style 
errorStyle) {
+        List<Span> spans = new ArrayList<>();
+        int notes = 0;
+        for (Integer line : problems.keySet()) {
+            if (noteLines.contains(line)) {
+                notes++;
+            }
+        }
+        int errors = problems.size() - notes;
+        if (errors > 0) {
+            spans.add(Span.styled(" errors: " + errors + " ", errorStyle));
+        }
+        if (notes > 0) {
+            spans.add(Span.styled(" notes: " + notes + " ", Theme.warning()));
+        }
+        return spans;
+    }
+
+    /** Package-private for tests: the lines, from 0, whose problem is a note. 
*/
+    Set<Integer> noteLines() {
+        return noteLines;
+    }
+
     private static void addAll(List<String> msgs, EndpointValidator validator, 
String content) {
         if (validator != null) {
             List<String> found = validator.validate(content);
@@ -2675,12 +2752,15 @@ class SourceViewer {
             String problem = viewErrors.get(selectedLine);
             if (problem != null) {
                 // the problem of the line goes before its documentation, as 
the Error panel of the editor shows it
-                int remaining = Math.max(0, viewDocArea.width() - " Error 
".length() - 3);
+                boolean note = noteLines.contains(selectedLine);
+                Style problemStyle = note ? Theme.warning() : Theme.error();
+                String kind = note ? " Note " : " Error ";
+                int remaining = Math.max(0, viewDocArea.width() - 
kind.length() - 3);
                 docLines.add(Line.from(
-                        Span.styled("───", Theme.error()),
-                        Span.styled(" Error ", Theme.error().bold()),
-                        Span.styled("─".repeat(remaining), Theme.error())));
-                docLines.add(Line.from(Span.styled(problem, Theme.error())));
+                        Span.styled("───", problemStyle),
+                        Span.styled(kind, problemStyle.bold()),
+                        Span.styled("─".repeat(remaining), problemStyle)));
+                docLines.add(Line.from(Span.styled(problem, problemStyle)));
                 docLines.add(Line.from(Span.styled("F4 edit   F9 next 
problem", Style.EMPTY.dim())));
             } else if (titleText != null) {
                 String prefix = "─── ";
@@ -2725,7 +2805,7 @@ class SourceViewer {
         if (!diffOverlay && !visibleErrors.isEmpty()) {
             // a block has one title at the top: the error count goes on the 
line of the file name, not instead of it
             Style errorStyle = 
Style.EMPTY.fg(dev.tamboui.style.Color.rgb(0xFF, 0x66, 0x66));
-            titleSpans.add(Span.styled(" errors: " + visibleErrors.size() + " 
", errorStyle));
+            titleSpans.addAll(problemCounts(visibleErrors, errorStyle));
         }
         Title posTitle;
         if (diffOverlay) {
@@ -2879,8 +2959,11 @@ class SourceViewer {
             for (int r = 0; r < editorArea.height(); r++) {
                 int lineIdx = editState.scrollRow() + r;
                 if (visibleErrors.containsKey(lineIdx)) {
+                    // a note on dark amber, an error on dark red
                     Style errorBg = 
Style.EMPTY.fg(dev.tamboui.style.Color.WHITE)
-                            .bg(dev.tamboui.style.Color.rgb(0x8B, 0x00, 0x00));
+                            .bg(noteLines.contains(lineIdx)
+                                    ? dev.tamboui.style.Color.rgb(0x80, 0x60, 
0x00)
+                                    : dev.tamboui.style.Color.rgb(0x8B, 0x00, 
0x00));
                     int screenY = editorArea.top() + r;
                     for (int x = textAreaRect.left(); x < textAreaRect.left() 
+ gutterWidth; x++) {
                         dev.tamboui.buffer.Cell cell = frame.buffer().get(x, 
screenY);
@@ -2897,7 +2980,7 @@ class SourceViewer {
             List<Line> docLines = new ArrayList<>();
             String titleText = null;
             if (cursorError != null) {
-                titleText = "Error";
+                titleText = noteLines.contains(editState.cursorRow()) ? "Note" 
: "Error";
             } else if (editDocEntries != null && !editDocEntries.isEmpty()) {
                 titleText = editDocEntries.get(0).title();
             }
@@ -2905,7 +2988,9 @@ class SourceViewer {
                 String prefix = "─── ";
                 String suffix = " ";
                 int remaining = Math.max(0, docArea.width() - prefix.length() 
- titleText.length() - suffix.length());
-                Style errorDim = 
Style.EMPTY.fg(dev.tamboui.style.Color.rgb(0xFF, 0x66, 0x66));
+                Style errorDim = noteLines.contains(editState.cursorRow())
+                        ? Theme.warning()
+                        : Style.EMPTY.fg(dev.tamboui.style.Color.rgb(0xFF, 
0x66, 0x66));
                 docLines.add(Line.from(
                         Span.styled(prefix, errorDim),
                         Span.styled(titleText, errorDim.bold()),
@@ -3743,9 +3828,10 @@ class SourceViewer {
         }
         if (currentRouteId == null) {
             if (!viewErrors.isEmpty()) {
-                return Title.from(Line.from(
-                        Span.styled(" Source [" + info + "] ", ts),
-                        Span.styled(" errors: " + viewErrors.size() + " ", 
Theme.error())));
+                List<Span> spans = new ArrayList<>();
+                spans.add(Span.styled(" Source [" + info + "] ", ts));
+                spans.addAll(problemCounts(viewErrors, Theme.error()));
+                return Title.from(Line.from(spans));
             }
             return Title.from(Span.styled(" Source [" + info + "] ", ts));
         }
@@ -3946,7 +4032,8 @@ class SourceViewer {
             spans.add(Span.styled(">> ", focused ? Theme.label().bold() : 
Theme.label().dim()));
             if (!prefix.isEmpty()) {
                 Style numberStyle = viewErrors.containsKey(lineIndex)
-                        ? Theme.error().bold() : focused ? 
Theme.label().bold() : Theme.label().dim();
+                        ? (noteLines.contains(lineIndex) ? 
Theme.warning().bold() : Theme.error().bold())
+                        : focused ? Theme.label().bold() : Theme.label().dim();
                 spans.add(Span.styled(prefix, numberStyle.patch(selBg)));
             }
             addLiveColumn(spans, lineIndex, selBg);
@@ -3955,7 +4042,9 @@ class SourceViewer {
             }
         } else {
             if (viewErrors.containsKey(lineIndex)) {
-                spans.add(Span.styled(" ✗ ", Theme.error().bold()));
+                spans.add(noteLines.contains(lineIndex)
+                        ? Span.styled(" ⚠ ", Theme.warning().bold())
+                        : Span.styled(" ✗ ", Theme.error().bold()));
             } else {
                 spans.add(isDeprecated
                         ? Span.styled(" ⚠ ", Theme.warning())
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
index ab8afd3c1911..ffff94e5d3f4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
@@ -230,10 +230,15 @@ class SyntaxHighlighter {
             return Language.PLAIN;
         }
         ext = ext.toLowerCase();
+        // the last extension: a Kamelet is name.kamelet.yaml, which onlyExt 
gives as kamelet.yaml (CAMEL-25411)
+        int dot = ext.lastIndexOf('.');
+        if (dot >= 0) {
+            ext = ext.substring(dot + 1);
+        }
         return switch (ext) {
             case "java" -> Language.JAVA;
-            case "yaml", "yml", "camel.yaml", "camel.yml" -> Language.YAML;
-            case "xml", "camel.xml" -> Language.XML;
+            case "yaml", "yml" -> Language.YAML;
+            case "xml" -> Language.XML;
             case "properties" -> Language.PROPERTIES;
             default -> Language.PLAIN;
         };
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
index 67b4620f950f..a8e046633e76 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
@@ -117,6 +117,68 @@ class SourceKameletValidationTest {
                 ROUTE.replace("tagg:", "tag:"))).isEmpty();
     }
 
+    @Test
+    void theParametersOfAKameletEndpointCompleteItsProperties() throws 
Exception {
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
KAMELET, StandardCharsets.UTF_8);
+        SourceEditAssist assist = new SourceEditAssist(
+                new MonitorContext(
+                        new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())));
+        List<AutocompletePopup.CompletionItem> items
+                = 
assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action",
 tempDir);
+        // the property of the Kamelet first, then the options of the kamelet 
component
+        assertThat(items.get(0).key()).isEqualTo("tag");
+        assertThat(items.get(0).required()).isTrue();
+        assertThat(items.get(0).group()).isEqualTo("kamelet tag-order-action");
+        
assertThat(items).extracting(AutocompletePopup.CompletionItem::key).contains("routeId",
 "timeout");
+        // a property already given is not offered again
+        
assertThat(assist.provideYamlKeyCompletions("yaml:kamelet:producer:tag|kamelet:tag-order-action",
 tempDir))
+                
.extracting(AutocompletePopup.CompletionItem::key).doesNotContain("tag");
+        // a Kamelet of the catalog, in the query of the uri
+        
assertThat(assist.provideYamlKeyCompletions("yaml:kamelet:consumer|kamelet:timer-source?period=1000",
 tempDir))
+                
.extracting(AutocompletePopup.CompletionItem::key).startsWith("message");
+    }
+
+    @Test
+    void theOptionsPopupIsNamedAfterTheKamelet() {
+        assertThat(SourceViewer.optionsTitle("kamelet", 
"kamelet:tag-order-action")).isEqualTo("tag-order-action options");
+        assertThat(SourceViewer.optionsTitle("kamelet", 
"kamelet:timer-source?period=1000"))
+                .isEqualTo("timer-source options");
+        assertThat(SourceViewer.optionsTitle("kamelet", 
null)).isEqualTo("kamelet options");
+        assertThat(SourceViewer.optionsTitle("timer", 
"timer:tick")).isEqualTo("timer options");
+    }
+
+    @Test
+    void aDividerSetsTheKameletComponentOptionsApart() throws Exception {
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
KAMELET, StandardCharsets.UTF_8);
+        SourceEditAssist assist = new SourceEditAssist(
+                new MonitorContext(
+                        new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())));
+        List<AutocompletePopup.CompletionItem> items
+                = 
assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action",
 tempDir);
+        // tag above the divider, the options of the kamelet component below it
+        assertThat(new AutocompletePopup(items, "", 
"").dividerAt()).isEqualTo(1);
+        // filtering keeps the property of the Kamelet above: "t" matches tag 
and timeout
+        AutocompletePopup filtered = new AutocompletePopup(items, "t", "t");
+        assertThat(filtered.dividerAt()).isEqualTo(1);
+        // only the Kamelet's properties left, or none: no divider
+        assertThat(new AutocompletePopup(items, "tag", 
"tag").dividerAt()).isEqualTo(-1);
+        assertThat(new AutocompletePopup(items, "routeId", 
"routeId").dividerAt()).isEqualTo(-1);
+        // a Kamelet without properties: only the options of the component, no 
divider
+        String noProperties = KAMELET.replace("""
+                    required:
+                      - tag
+                    properties:
+                      tag:
+                        title: Tag
+                        type: string
+                """, "").replace("{{tag}}", "tagged");
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
noProperties, StandardCharsets.UTF_8);
+        List<AutocompletePopup.CompletionItem> none
+                = 
assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action",
 tempDir);
+        assertThat(none).isNotEmpty();
+        assertThat(new AutocompletePopup(none, "", 
"").dividerAt()).isEqualTo(-1);
+    }
+
     @Test
     void aKameletFileWithAProblemIsMarkedOnLoadAndNotSaved() throws Exception {
         Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
@@ -143,6 +205,8 @@ class SourceKameletValidationTest {
         viewer.loadFile(file);
         int timerLine = lineOf(timer, "\"camel:timer\"");
         assertThat(viewer.viewErrors()).containsOnlyKeys(timerLine);
+        // a note, marked as a warning and counted apart from the errors
+        assertThat(viewer.noteLines()).containsOnly(timerLine);
 
         viewer.enterEditMode();
         appendSpaceToLine(viewer, 1);
@@ -151,6 +215,42 @@ class SourceKameletValidationTest {
         assertThat(lastNotification.get()).startsWith("Saved: 
tag-order-action.kamelet.yaml with 1 Camel problem: ")
                 .contains("camel:timer is not used by the template");
         assertThat(viewer.inlineErrors()).containsOnlyKeys(timerLine);
+        assertThat(viewer.noteLines()).containsOnly(timerLine);
+    }
+
+    @Test
+    void shiftF9RemovesAnUnusedDependency() throws Exception {
+        Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
+        // camel:timer the only dependency, as the model wrote it: the 
dependencies: key goes with it
+        String timer = KAMELET.replace("    - \"camel:kamelet\"\n", "    - 
\"camel:timer\"\n");
+        Files.writeString(file, timer, StandardCharsets.UTF_8);
+        SourceViewer viewer = viewer(file);
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+        for (int i = 0; i < lineOf(timer, "\"camel:timer\""); i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F9, KeyModifiers.SHIFT));
+        
assertThat(viewer.editText()).doesNotContain("camel:timer").doesNotContain("dependencies:")
+                .contains("  template:");
+        assertThat(lastNotification.get()).isEqualTo("Fixed: remove 
camel:timer");
+    }
+
+    @Test
+    void anErrorIsNotANote() throws Exception {
+        Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
+        String self = KAMELET.replace("uri: kamelet:source", "uri: 
kamelet:tag-order-action");
+        Files.writeString(file, self, StandardCharsets.UTF_8);
+        SourceViewer viewer = viewer(file);
+        viewer.loadFile(file);
+        assertThat(viewer.viewErrors()).isNotEmpty();
+        assertThat(viewer.noteLines()).isEmpty();
+    }
+
+    @Test
+    void theTemplateOfAKameletIsNamedAfterIt() {
+        
assertThat(SourceTab.kameletName(List.of(KAMELET.split("\n")))).isEqualTo("tag-order-action");
+        assertThat(SourceTab.kameletName(List.of(ROUTE.split("\n")))).isNull();
     }
 
     private SourceViewer viewer(Path file) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
index 8361a9df4a22..13be5dae4a48 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
@@ -88,6 +88,58 @@ class SourceTabJumpLinksTest {
         assertThat(links.get(15).routeId()).isEqualTo("direct:review");
     }
 
+    @Test
+    void aKameletUsedByTwoRoutesLinksToAPopupOfItsCallers() throws Exception {
+        // CAMEL-25411: the template of a Kamelet is the route of kamelet:<its 
name>
+        Path kamelet = 
Files.writeString(dir.resolve("tag-order-action.kamelet.yaml"), """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: tag-order-action
+                spec:
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - setBody:
+                            simple: "${body} [{{tag}}]"
+                """);
+        Path orders = Files.writeString(dir.resolve("orders.camel.yaml"), """
+                - route:
+                    id: orders
+                    from:
+                      uri: timer:orders
+                      steps:
+                        - to:
+                            uri: kamelet:tag-order-action
+                """);
+        SourceTab tab = newTab();
+        assertThat(tab.loadDirectory(dir)).isTrue();
+        // the route links to the Kamelet, and one caller is linked from the 
template
+        
assertThat(tab.computeJumpLinks(orders).get(6).routeId()).isEqualTo("tag-order-action");
+        
assertThat(tab.computeJumpLinks(kamelet).get(6).routeId()).isEqualTo("orders");
+
+        Files.writeString(dir.resolve("refunds.camel.yaml"), """
+                - route:
+                    id: refunds
+                    from:
+                      uri: timer:refunds
+                      steps:
+                        - to:
+                            uri: kamelet:tag-order-action
+                """);
+        assertThat(tab.loadDirectory(dir)).isTrue();
+        SourceViewer.JumpLink link = tab.computeJumpLinks(kamelet).get(6);
+        assertThat(link.routeId()).isEqualTo("2 callers");
+        assertThat(link.callersOf()).isEqualTo("kamelet:tag-order-action");
+        assertThat(tab.callersOf("kamelet:tag-order-action"))
+                
.extracting(GotoRoutePopup.RouteItem::routeId).containsExactlyInAnyOrder("orders",
 "refunds");
+        // the usages popup opened on the from: line of the template does not 
list that line itself
+        List<GotoRoutePopup.RouteItem> usages = SourceTab.withoutLine(
+                tab.usagesOf("kamelet:tag-order-action"), kamelet.toString(), 
6);
+        
assertThat(usages).extracting(GotoRoutePopup.RouteItem::routeId).containsExactlyInAnyOrder("orders",
 "refunds");
+    }
+
     @Test
     void javaRoutesLinkToAndFromYamlRoutes() throws Exception {
         Path orders = Files.writeString(dir.resolve("Orders.java"), """
@@ -122,10 +174,12 @@ class SourceTabJumpLinksTest {
         assertThat(links.get(5).filePath()).isEqualTo(billing.toString());
         assertThat(links.get(9).routeId()).isEqualTo("billing");
 
-        // and back: the from: line of the YAML route links to a Java caller
+        // and back: the from: line of the YAML route has two Java callers, to 
choose from in a popup (CAMEL-25411)
         SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
-        assertThat(back.filePath()).isEqualTo(orders.toString());
-        assertThat(back.routeId()).isEqualTo("file:inbox");
+        assertThat(back.routeId()).isEqualTo("2 callers");
+        assertThat(back.callersOf()).isEqualTo("direct:billing");
+        
assertThat(tab.callersOf("direct:billing")).extracting(GotoRoutePopup.RouteItem::filePath)
+                .containsOnly(orders.toString());
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
index 8d58ab215013..162c415b7ea7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighterTest.java
@@ -44,6 +44,14 @@ class SyntaxHighlighterTest {
                 SyntaxHighlighter.detectLanguage("application.properties:12"));
     }
 
+    @Test
+    void detectsTheLanguageOfAKameletFile() {
+        // onlyExt gives kamelet.yaml: the last extension decides (CAMEL-25411)
+        assertEquals(SyntaxHighlighter.Language.YAML, 
SyntaxHighlighter.detectLanguage("tag-order-action.kamelet.yaml"));
+        assertEquals(SyntaxHighlighter.Language.YAML, 
SyntaxHighlighter.detectLanguage("orders.camel.yaml"));
+        assertEquals(SyntaxHighlighter.Language.XML, 
SyntaxHighlighter.detectLanguage("orders.camel.xml"));
+    }
+
     @Test
     void colorsKeySeparatorAndValue() {
         Line line = SyntaxHighlighter.highlightLine("camel.main.name=demo", 
SyntaxHighlighter.Language.PROPERTIES);

Reply via email to