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 6463f13daf57 chore: camel-jbang - the AI overview leaves a folder of 
projects and the home folder alone
6463f13daf57 is described below

commit 6463f13daf57fe61918e218e8d66b3df70c24b4b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 15 11:09:15 2026 +0200

    chore: camel-jbang - the AI overview leaves a folder of projects and the 
home folder alone
    
    With AI Overview set to auto, opening the AI panel without a selected
    integration explained the folder the TUI was started in. Started in a
    workspace with a checkout of each repository, that took the routes of the
    first 2000 files across all of them as one project and wrote a
    camel-summary.md about them.
    
    ProjectFolders tells such a folder apart: the home folder, a root of the
    file system, or a folder that is no project itself (no repository, build,
    Camel configuration or route file at its top) while two or more of its
    subfolders are. Auto mode leaves it alone and says so once in the panel,
    and /overview says why instead of asking the model.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui-ai.adoc     |   4 +-
 .../jbang/core/commands/tui/ProjectFolders.java    | 115 +++++++++++++++++++++
 .../core/commands/tui/ProjectOverviewAssist.java   |  14 +++
 .../core/commands/tui/ProjectFoldersTest.java      |  86 +++++++++++++++
 .../commands/tui/ProjectOverviewAssistTest.java    |  26 +++++
 5 files changed, 244 insertions(+), 1 deletion(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
index cecff8130bd0..ebfc32f35e8c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
@@ -118,7 +118,9 @@ cycles backward.
 A route diagram of many routes becomes spaghetti quickly, above all when the 
routes have no
 `description`. `/overview` asks the model to explain the project of the 
selected integration (or the
 folder the TUI runs in): an overview of what it does, the business 
capabilities its routes implement,
-and a description of each route that has none. The facts come from the route 
sources, the same ones
+and a description of each route that has none. The folder the TUI runs in is 
explained only when it is a
+project: not your home folder, nor a folder that holds several projects, such 
as a workspace with a
+checkout of each repository. There the panel says so, and selecting an 
integration explains its project. The facts come from the route sources, the 
same ones
 `camel overview` prints (see 
xref:camel-jbang-ai.adoc#_project_overview[Project overview]): routes,
 entry points, how routes connect, external systems and findings. The model 
gets them with excerpts of
 the routes (values of passwords, tokens and other sensitive options masked) in 
one request without
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectFolders.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectFolders.java
new file mode 100644
index 000000000000..e3a5dabe0574
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectFolders.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+
+/**
+ * Tells a project folder from a folder that is not one project. The AI 
project overview explains one project: started
+ * in the home folder, or in a workspace with a checkout of each repository, 
it would explain the routes it happens to
+ * find across all of them as if they were one.
+ */
+final class ProjectFolders {
+
+    /** What makes a folder a project of its own: a repository, a build, or a 
Camel configuration. */
+    private static final List<String> PROJECT_FILES = List.of(
+            ".git", "pom.xml", "build.gradle", "build.gradle.kts", 
"application.properties", "application.yaml",
+            "application.yml");
+    /** The route files a Camel JBang project keeps at the top of its folder. 
*/
+    private static final List<String> ROUTE_EXTENSIONS = List.of(".yaml", 
".yml", ".xml", ".java", ".groovy");
+    /** How many of the projects a folder holds are named. */
+    private static final int NAMED = 3;
+
+    private ProjectFolders() {
+    }
+
+    /**
+     * Why a folder is not one project to explain, or null when it is one. It 
is not when it is the home folder or a
+     * root of the file system, or when it is no project itself (no 
repository, build, Camel configuration or route file
+     * at its top) while two or more of its subfolders are, as in a workspace 
of checkouts.
+     */
+    static String notOneProject(Path dir) {
+        return notOneProject(dir, CommandLineHelper.getHomeDir());
+    }
+
+    /** As {@link #notOneProject(Path)}, with the home folder given. */
+    static String notOneProject(Path dir, Path home) {
+        if (dir == null) {
+            return null;
+        }
+        Path path = dir.toAbsolutePath().normalize();
+        if (path.getParent() == null) {
+            return path + " is the root of the file system";
+        }
+        if (home != null && path.equals(home.toAbsolutePath().normalize())) {
+            return AcpHeaderStrip.homeRelative(path) + " is your home folder";
+        }
+        if (isProject(path, true)) {
+            return null;
+        }
+        List<String> projects = new ArrayList<>();
+        try (Stream<Path> children = Files.list(path)) {
+            children.filter(Files::isDirectory)
+                    .filter(d -> !d.getFileName().toString().startsWith("."))
+                    .filter(d -> isProject(d, false))
+                    .map(d -> d.getFileName().toString())
+                    .sorted()
+                    .forEach(projects::add);
+        } catch (IOException e) {
+            return null;
+        }
+        if (projects.size() < 2) {
+            return null;
+        }
+        String named = String.join(", ", projects.subList(0, Math.min(NAMED, 
projects.size())));
+        if (projects.size() > NAMED) {
+            named += " and " + (projects.size() - NAMED) + " more";
+        }
+        return AcpHeaderStrip.homeRelative(path) + " holds " + projects.size() 
+ " projects (" + named + ")";
+    }
+
+    /**
+     * Whether a folder is a project: a repository, build or Camel 
configuration at its top, or (when
+     * {@code routeFiles}) a route file there. Subfolders are not judged by 
route files, as a project may keep its
+     * routes in a few folders of its own.
+     */
+    private static boolean isProject(Path dir, boolean routeFiles) {
+        for (String name : PROJECT_FILES) {
+            if (Files.exists(dir.resolve(name))) {
+                return true;
+            }
+        }
+        if (!routeFiles) {
+            return false;
+        }
+        try (Stream<Path> children = Files.list(dir)) {
+            return children.filter(Files::isRegularFile)
+                    .map(f -> 
f.getFileName().toString().toLowerCase(Locale.ROOT))
+                    .anyMatch(n -> !n.startsWith(".") && 
ROUTE_EXTENSIONS.stream().anyMatch(n::endsWith));
+        } catch (IOException e) {
+            return true;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
index a6e4a1e3fc61..1287db9e8de2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
@@ -101,6 +101,11 @@ final class ProjectOverviewAssist {
         if (dir == null) {
             return "No project directory: select an integration, or start the 
TUI in a project folder.";
         }
+        String notOne = ProjectFolders.notOneProject(dir);
+        if (notOne != null) {
+            return notOne + ", not one project: select an integration to 
explain its project, or start the TUI in a"
+                   + " project folder.";
+        }
         String arg = arguments == null ? "" : 
arguments.strip().toLowerCase(Locale.ROOT);
         if (running.get()) {
             return "The project overview is still being written.";
@@ -158,6 +163,15 @@ final class ProjectOverviewAssist {
         if (!MODE_AUTO.equals(mode()) || dir == null || client == null || 
running.get()) {
             return false;
         }
+        String notOne = ProjectFolders.notOneProject(dir);
+        if (notOne != null) {
+            // said once a session: the routes of a workspace of checkouts, or 
of the home folder, are not one project
+            if (autoDone.add(dir + "|not one project")) {
+                sink.add(AiRole.SYSTEM, notOne + ", so AI Overview: auto 
leaves it alone; select an integration to"
+                                        + " explain its project.");
+            }
+            return false;
+        }
         ProjectOverview.Overview overview = ProjectOverview.analyze(dir, 
catalog());
         if (overview.flows().isEmpty() || !autoDone.add(dir + "|" + 
overview.fingerprint())) {
             return false;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectFoldersTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectFoldersTest.java
new file mode 100644
index 000000000000..ea78d54fb7d6
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectFoldersTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ProjectFoldersTest {
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void aWorkspaceOfCheckoutsIsNotOneProject() throws IOException {
+        // a repository, a Maven build, a Camel JBang project, and two folders 
that are none
+        Files.createDirectories(dir.resolve("camel/.git"));
+        
Files.writeString(Files.createDirectories(dir.resolve("camel-spring-boot")).resolve("pom.xml"),
 "<project/>");
+        
Files.writeString(Files.createDirectories(dir.resolve("orders")).resolve("application.properties"),
 "");
+        Files.createDirectories(dir.resolve("inbox"));
+        Files.createDirectories(dir.resolve(".idea"));
+        Files.writeString(dir.resolve("notes.md"), "# notes");
+
+        assertThat(ProjectFolders.notOneProject(dir))
+                .endsWith(" holds 3 projects (camel, camel-spring-boot, 
orders)");
+    }
+
+    @Test
+    void manyProjectsAreCountedAndTheFirstNamed() throws IOException {
+        for (String name : new String[] { "a", "b", "c", "d", "e" }) {
+            Files.createDirectories(dir.resolve(name + "/.git"));
+        }
+        assertThat(ProjectFolders.notOneProject(dir)).endsWith(" holds 5 
projects (a, b, c and 2 more)");
+    }
+
+    @Test
+    void aFolderWithItsOwnBuildOrRoutesIsOneProject() throws IOException {
+        Files.createDirectories(dir.resolve("module-a/.git"));
+        Files.createDirectories(dir.resolve("module-b/.git"));
+        // a Camel JBang project keeps its routes at the top
+        Files.writeString(dir.resolve("orders.camel.yaml"), "- route:\n");
+        assertThat(ProjectFolders.notOneProject(dir)).isNull();
+
+        // and a repository with its own sub-repositories is still one project
+        Files.delete(dir.resolve("orders.camel.yaml"));
+        Files.createDirectories(dir.resolve(".git"));
+        assertThat(ProjectFolders.notOneProject(dir)).isNull();
+    }
+
+    @Test
+    void aFolderWithOneProjectOrNoneIsLeftToTheOverview() throws IOException {
+        // what it holds may be the routes of the project, kept in folders of 
their own
+        Files.createDirectories(dir.resolve("routes"));
+        Files.writeString(dir.resolve("routes/orders.yaml"), "- route:\n");
+        Files.createDirectories(dir.resolve("library/.git"));
+        assertThat(ProjectFolders.notOneProject(dir)).isNull();
+    }
+
+    @Test
+    void theHomeFolderAndTheRootAreNeverOneProject() throws IOException {
+        // whatever the home folder holds, even routes at its top
+        Files.writeString(dir.resolve("orders.camel.yaml"), "- route:\n");
+        assertThat(ProjectFolders.notOneProject(dir, dir)).endsWith(" is your 
home folder");
+        assertThat(ProjectFolders.notOneProject(dir.getRoot(), 
dir)).endsWith(" is the root of the file system");
+        assertThat(ProjectFolders.notOneProject(null, dir)).isNull();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssistTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssistTest.java
index 6b53aa29d340..f9940102820d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssistTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssistTest.java
@@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.api.parallel.Isolated;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -251,6 +252,31 @@ class ProjectOverviewAssistTest {
         assertTrue(assist.command("", project, null, null, 
sink).startsWith("No routes found"));
     }
 
+    @Test
+    void aFolderOfProjectsIsNotExplainedAsOne(@TempDir Path workspace) throws 
Exception {
+        TuiSettings settings = TuiSettings.load();
+        settings.setAiOverview("auto");
+        settings.save();
+        // a workspace with a checkout of each repository, as the TUI started 
there sees it
+        Files.createDirectories(workspace.resolve("camel/.git"));
+        Files.createDirectories(workspace.resolve("camel-website/.git"));
+        Files.writeString(workspace.resolve("camel/orders.camel.yaml"), 
ROUTES, StandardCharsets.UTF_8);
+        AnsweringClient client = new AnsweringClient(ANSWER);
+        ProjectOverviewAssist assist = new ProjectOverviewAssist();
+
+        // auto mode leaves it alone, and says so once
+        assertThat(assist.autoExplain(workspace, client, null, 
sink)).isFalse();
+        assertThat(assist.autoExplain(workspace, client, null, 
sink)).isFalse();
+        assertThat(entries).singleElement().asString()
+                .contains("holds 2 projects (camel, camel-website)")
+                .contains("AI Overview: auto leaves it alone");
+
+        // and /overview tells why instead of asking the model
+        assertThat(assist.command("", workspace, client, null, 
sink)).contains("not one project");
+        assertThat(client.calls).hasValue(0);
+        
assertThat(workspace.resolve(IntegrationSummary.FILE_NAME)).doesNotExist();
+    }
+
     private void rewrite(String content) {
         try {
             Files.writeString(project.resolve("orders.camel.yaml"), content, 
StandardCharsets.UTF_8);

Reply via email to