This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch tui-walk-examples
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 55da89e6cd063a6b3665d2f54683f3d1576eb7f7
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 23:59:01 2026 +0100

    chore: camel-jbang - About says what an integration does from its README 
too, opens the README and the summary, and shows the period of a timer
    
    From a walk of the quick-start and run examples in the TUI:
    
    - About: without a comment at the top of a route file, the purpose is
      the first paragraph of the README, then the descriptions of the
      routes, then the overview of camel-summary.md (AI-assisted). The four
      quick-start examples, memory-leak and tui-hello-world had none.
      r opens the README and s the summary of the project in the viewer.
    - About: In shows the period or schedule a timer or cron runs with, from
      the properties of the integration with its profile applied, not
      {{welcome.period}}. The hint names the file of the first route, not
      beans.yaml.
    - The summary is named as the integration, not the directory an example
      runs from (camel-example-7061...).
    - The shell, the AI panel and the MCP answers get the full name of the
      selected integration; it was shortened to 20 characters for the
      title, so camel commands in the shell missed properties-and-profiles.
    - Overview: no 0ms/0ms/0ms before the first exchange.
    - The CPU average of an integration starts at 0, so the start of its JVM
      is no 500% average of the last 15 minutes.
    - The diagram zoom is saved one at a time, and AbstractDiagramTabTest no
      longer writes into the settings of who runs it (it failed now and then
      on the order of the background saves).
    - ExamplesSayWhatTheyDoTest: every bundled example says what it does.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        | 10 ++-
 .../camel/dsl/jbang/core/commands/ai/About.java    | 89 +++++++++++++++++++++-
 .../dsl/jbang/core/commands/ai/AboutTest.java      | 69 +++++++++++++++++
 .../commands/ai/ExamplesSayWhatTheyDoTest.java     | 60 +++++++++++++++
 .../dsl/jbang/core/commands/tui/AboutView.java     | 76 ++++++++++++++----
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  2 +-
 .../core/commands/tui/IntegrationSummaryDoc.java   | 20 ++++-
 .../camel/dsl/jbang/core/commands/tui/LoadAvg.java | 18 ++++-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  2 +-
 .../jbang/core/commands/tui/MetricsCollector.java  |  2 +-
 .../jbang/core/commands/tui/MonitorContext.java    | 14 ++++
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  2 +-
 .../dsl/jbang/core/commands/tui/Placeholders.java  | 59 ++++++++++++++
 .../jbang/core/commands/tui/RouteDiagramZoom.java  |  8 +-
 .../dsl/jbang/core/commands/tui/ShellPanel.java    |  2 +-
 .../src/main/resources/tui/help/overview.md        | 12 ++-
 .../dsl/jbang/core/commands/tui/AboutViewTest.java | 49 ++++++++++++
 .../core/commands/tui/AbstractDiagramTabTest.java  | 30 +++++++-
 .../commands/tui/IntegrationSummaryDocTest.java    | 17 +++++
 .../dsl/jbang/core/commands/tui/LoadAvgTest.java   | 12 +++
 .../core/commands/tui/McpFacadeControlTest.java    | 10 +++
 .../core/commands/tui/OverviewTabRenderTest.java   | 20 +++++
 .../jbang/core/commands/tui/PlaceholdersTest.java  | 55 +++++++++++++
 23 files changed, 602 insertions(+), 36 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index e72d184e60fc..d4076ea74616 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -24,10 +24,12 @@ happened to it, with no AI:
 
 image::jbang/camel-tui-about.png[About the chaos demo: its purpose, what comes 
in, its routes and what goes out, the last 5 minutes after an outage of the 
stock service, and a line per route of what happened]
 
-* The header has the Camel version, runtime, files, routes and uptime, and the 
purpose: the comment at the top of the
-main source file (a YAML `#` comment, a Javadoc or `//` comment in Java, an 
XML comment), else the descriptions of the
-routes.
-* *In*, *Routes* and *Out* say what starts it, its routes (a route called with 
`direct` sits under its caller with `↳`,
+* The header has the Camel version, runtime, files, routes and uptime, and the 
purpose: the comment at the top of a
+route file (a YAML `#` comment, a Javadoc or `//` comment in Java, an XML 
comment), else the first paragraph of the
+README, else the descriptions of the routes, else the overview an AI wrote in 
`camel-summary.md`. *r* opens the README,
+and *s* the summary of the project.
+* *In*, *Routes* and *Out* say what starts it (a timer or a cron with the 
period or schedule of its properties, its
+profile applied), its routes (a route called with `direct` sits under its 
caller with `↳`,
 one handed off with `seda` with `⇢`) and what it talks to, with the exchanges 
since it started and a sparkline of the
 last 5 minutes. *Up* and *Down* select a line, and *Enter* opens a route 
drilled down on the Diagram, or an endpoint with
 its details on the Endpoint tab.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/About.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/About.java
index 8e4f3aeb1101..391987e36e6e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/About.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/About.java
@@ -24,8 +24,10 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Stream;
 
 /**
  * About an integration (CAMEL-26700): what it does at a glance and without an 
AI, from the analysis of its sources
@@ -78,6 +80,11 @@ public final class About {
     public record Model(String purpose, String purposeFrom, List<In> in, 
List<RouteLine> routes, List<Out> out) {
     }
 
+    // the names of a README, in the order they are taken
+    private static final List<String> README_NAMES = List.of("readme.md", 
"readme.adoc", "readme.txt", "readme");
+    // shorter than this is no description (a link, a word)
+    private static final int MIN_PARAGRAPH = 20;
+
     private About() {
     }
 
@@ -143,8 +150,9 @@ public final class About {
 
     /**
      * What the integration does, in the words of its author: the comment at 
the top of a route file (the files in the
-     * order of the analysis), else the descriptions of the routes that 
something outside starts. Returns the text and
-     * where it comes from, or null.
+     * order of the analysis), else the first paragraph of its README, else 
the descriptions of the routes that
+     * something outside starts, else the overview an AI wrote in its 
camel-summary.md. Returns the text and where it
+     * comes from, or null.
      */
     public static String[] purpose(Path dir, ProjectOverview.Overview o) {
         for (String file : o.files()) {
@@ -157,6 +165,17 @@ public final class About {
                 // an unreadable file has no comment
             }
         }
+        Path readme = readme(dir);
+        if (readme != null) {
+            try {
+                String text = firstParagraph(Files.readString(readme, 
StandardCharsets.UTF_8));
+                if (text != null) {
+                    return new String[] { text, "from " + readme.getFileName() 
};
+                }
+            } catch (IOException e) {
+                // an unreadable README says nothing
+            }
+        }
         List<String> descriptions = new ArrayList<>();
         Set<String> entryRoutes = new LinkedHashSet<>();
         o.entryPoints().forEach(e -> entryRoutes.add(e.route()));
@@ -175,9 +194,75 @@ public final class About {
                     String.join(" ", descriptions.subList(0, Math.min(2, 
descriptions.size()))),
                     "from the descriptions of the routes" };
         }
+        IntegrationSummary.Summary summary = IntegrationSummary.read(dir);
+        String overview = summary != null ? firstParagraph(summary.overview()) 
: null;
+        if (overview != null) {
+            return new String[] { overview, "from " + 
IntegrationSummary.FILE_NAME + " (AI-assisted)" };
+        }
+        return null;
+    }
+
+    /** The README of the project: README.md, README.adoc, README.txt or 
README, whatever their case; or null. */
+    public static Path readme(Path dir) {
+        if (dir == null || !Files.isDirectory(dir)) {
+            return null;
+        }
+        try (Stream<Path> files = Files.list(dir)) {
+            Map<String, Path> byName = new LinkedHashMap<>();
+            files.filter(Files::isRegularFile)
+                    .forEach(f -> 
byName.putIfAbsent(f.getFileName().toString().toLowerCase(Locale.ROOT), f));
+            for (String name : README_NAMES) {
+                if (byName.containsKey(name)) {
+                    return byName.get(name);
+                }
+            }
+        } catch (IOException e) {
+            // no README to read
+        }
         return null;
     }
 
+    /**
+     * The first paragraph of a Markdown or AsciiDoc text, as plain text: 
after its title, without badges, images,
+     * attributes and comments, and with the markup of code, emphasis and 
links taken out; null when it has none.
+     */
+    public static String firstParagraph(String text) {
+        if (text == null) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        boolean comment = false;
+        for (String line : text.lines().toList()) {
+            String l = line.strip();
+            if (comment || l.startsWith("<!--")) {
+                comment = !l.contains("-->");
+                continue;
+            }
+            if (l.isEmpty() || l.startsWith("#") || l.startsWith("=") || 
l.startsWith("```") || l.startsWith("----")
+                    || l.startsWith("- ") || l.startsWith("* ") || 
l.startsWith("|") || l.startsWith(">")) {
+                if (!sb.isEmpty()) {
+                    break;
+                }
+                continue;
+            }
+            if (l.startsWith("[!") || l.startsWith("[[") || 
l.matches("\\[[^]]*]") || l.startsWith("![")
+                    || l.startsWith(":") || l.startsWith("//") || 
l.startsWith("image:") || l.startsWith("<")) {
+                // badges, images, attributes, anchors and comments of 
AsciiDoc, HTML
+                continue;
+            }
+            sb.append(l).append(' ');
+        }
+        String p = sb.toString()
+                .replaceAll("\\[([^\\]]*)]\\([^)]*\\)", "$1") // [text](url)
+                .replaceAll("(?:https?|link):[^\\s\\[]*\\[([^\\]]*)]", "$1") 
// link:url[text] and url[text]
+                .replaceAll("[`*]+|(?<!\\w)_+|_+(?!\\w)", "")
+                .replaceAll("\\s+", " ").strip();
+        if (p.length() < MIN_PARAGRAPH) {
+            return null;
+        }
+        return p.length() > 300 ? p.substring(0, 297) + "…" : p;
+    }
+
     /**
      * The comment a file starts with: the {@code #} lines of YAML, the 
Javadoc or {@code //} lines before the class of
      * Java, the {@code <!-- -->} of XML before its first element. A license 
header is no description: the comment after
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
index e9ce35a5ac45..73b26d7c27c3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
@@ -89,6 +89,75 @@ class AboutTest {
         assertThat(About.leadingComment("a.yaml", null)).isNull();
     }
 
+    @Test
+    void withoutACommentThePurposeIsTheFirstParagraphOfTheReadme() throws 
Exception {
+        // quick-start/routes: the bean file comes first, and neither file has 
a comment
+        Files.writeString(dir.resolve("beans.yaml"), "- beans:\n    - name: 
greeter\n      type: camel.example.Greeter\n");
+        Files.writeString(dir.resolve("routes.camel.yaml"), """
+                - route:
+                    id: greeting
+                    from:
+                      uri: timer:greet
+                      steps:
+                        - log: hello
+                """);
+        assertThat(About.of(dir, ProjectOverview.analyze(dir, 
CATALOG)).purpose()).isNull();
+
+        // what the AI wrote comes last
+        Files.writeString(dir.resolve(IntegrationSummary.FILE_NAME), """
+                # greeting
+
+                ## Overview ✦ AI-assisted
+
+                <!-- ai:begin -->
+                A timer greets every second.
+
+                It has one route.
+                <!-- ai:end -->
+                """);
+        About.Model m = About.of(dir, ProjectOverview.analyze(dir, CATALOG));
+        assertThat(m.purpose()).isEqualTo("A timer greets every second.");
+        assertThat(m.purposeFrom()).isEqualTo("from camel-summary.md 
(AI-assisted)");
+
+        // the author's words before: the README
+        Files.writeString(dir.resolve("README.md"), """
+                # Routes
+
+                [![build](https://example.org/badge.svg)](https://example.org)
+
+                The first step from YAML into your own code: a timer route in 
YAML calls a Java bean, `Greeter`, that
+                builds the message, and logs what the [bean](Greeter.java) 
returned.
+
+                ## What you will see
+                """);
+        m = About.of(dir, ProjectOverview.analyze(dir, CATALOG));
+        assertThat(m.purpose()).isEqualTo("The first step from YAML into your 
own code: a timer route in YAML calls a"
+                                          + " Java bean, Greeter, that builds 
the message, and logs what the bean"
+                                          + " returned.");
+        assertThat(m.purposeFrom()).isEqualTo("from README.md");
+        assertThat(About.readme(dir)).isEqualTo(dir.resolve("README.md"));
+    }
+
+    @Test
+    void theFirstParagraphOfAsciiDoc() {
+        assertThat(About.firstParagraph("""
+                = Orders
+                :toc:
+
+                // the shop
+                [NOTE]
+                Each order is reserved, paid and shipped, see 
link:docs/flow.adoc[the flow] and *the* _routes_.
+
+                == Run
+                """)).isEqualTo("Each order is reserved, paid and shipped, see 
the flow and the routes.");
+        // a heading or a list only, or too short to say anything
+        assertThat(About.firstParagraph("# Title\n\n- one\n- two\n")).isNull();
+        assertThat(About.firstParagraph("# Title\n\nSee below.\n")).isNull();
+        assertThat(About.firstParagraph(null)).isNull();
+        // snake_case is no emphasis
+        assertThat(About.firstParagraph("Reads the order_lines table of the 
shop database.")).contains("order_lines");
+    }
+
     @Test
     void thePurposeIsTheCommentAfterTheLicenseHeader() {
         String yaml = "# Licensed to the Apache Software Foundation (ASF)\n# 
under the License.\n\n"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExamplesSayWhatTheyDoTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExamplesSayWhatTheyDoTest.java
new file mode 100644
index 000000000000..828cbd0b12cc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExamplesSayWhatTheyDoTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ai;
+
+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.stream.Stream;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Every bundled example says what it does in its own sources, as the About 
view of the TUI and camel get about show it
+ * first: the comment at the top of a route file, else the descriptions of its 
routes.
+ */
+class ExamplesSayWhatTheyDoTest {
+
+    private static final Path EXAMPLES = 
Path.of("src/main/resources/examples");
+
+    @Test
+    void everyExampleHasAPurpose() throws IOException {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        List<String> without = new ArrayList<>();
+        int examples = 0;
+        try (Stream<Path> groups = Files.list(EXAMPLES)) {
+            for (Path group : 
groups.filter(Files::isDirectory).sorted().toList()) {
+                try (Stream<Path> dirs = Files.list(group)) {
+                    for (Path dir : dirs.filter(d -> 
Files.isRegularFile(d.resolve("README.md"))).sorted().toList()) {
+                        examples++;
+                        if (About.purpose(dir, ProjectOverview.analyze(dir, 
catalog)) == null) {
+                            without.add(EXAMPLES.relativize(dir).toString());
+                        }
+                    }
+                }
+            }
+        }
+        assertThat(examples).isGreaterThan(25);
+        assertThat(without).as("examples without a comment at the top of a 
route file or a README").isEmpty();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 9d83e720e76d..855c13a5085d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Instant;
 import java.time.ZoneId;
@@ -49,6 +52,7 @@ import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.dsl.jbang.core.commands.ai.About;
 import org.apache.camel.dsl.jbang.core.commands.ai.AboutReport;
 import org.apache.camel.dsl.jbang.core.commands.ai.ProjectOverview;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -91,6 +95,8 @@ final class AboutView {
     private volatile boolean loading;
     private volatile String error;
     private Path dir;
+    // the README of the project, which r opens; null when it has none
+    private Path readme;
     private long loadedAt;
     // the zoom of What happened, as the windows of the Timeline tab (+ and 
-), and its last second when moved (← →);
     // -1 follows the last second recorded
@@ -147,7 +153,9 @@ final class AboutView {
             try {
                 ProjectOverview.Overview o = ProjectOverview.analyze(d, 
ArchitectureView.catalog());
                 About.Model m = About.of(d, o);
+                Path r = About.readme(d);
                 onRenderThread.accept(() -> {
+                    readme = r;
                     overview = o;
                     model = m;
                     purpose = m.purpose();
@@ -177,6 +185,14 @@ final class AboutView {
         if (ke.isConfirm()) {
             return open();
         }
+        if (ke.isChar('r') && readme != null) {
+            return openReadme(info);
+        }
+        if (ke.isChar('s') && dir != null) {
+            // the summary of the project, as the Diagram opens it: what an AI 
wrote, and what its sources say
+            IntegrationSummaryDoc.open(ctx, dir, null);
+            return true;
+        }
         long end = ctx.timeline.endSecond(info.pid);
         if (ke.isChar('+') || ke.isChar('=')) {
             if (zoom > 0 && end >= 0) {
@@ -206,6 +222,22 @@ final class AboutView {
         return false;
     }
 
+    /** Opens the README of the project in the document viewer. */
+    private boolean openReadme(IntegrationInfo info) {
+        if (ctx.openMarkdownCallback == null) {
+            return false;
+        }
+        try {
+            String text = Files.readString(readme, StandardCharsets.UTF_8);
+            String name = readme.getFileName().toString();
+            String markdown = name.toLowerCase(Locale.ROOT).endsWith(".adoc") 
? DocHelper.asciidocToMarkdown(text) : text;
+            ctx.openMarkdownCallback.accept((info.name != null ? info.name : 
info.pid) + " - " + name, markdown);
+        } catch (IOException e) {
+            error = "Cannot read " + readme + ": " + e.getMessage();
+        }
+        return true;
+    }
+
     /** The lines the cursor can be on, for an agent (tui_get_state): the 
route, or the endpoint and its route. */
     SelectionContext selectionContext() {
         List<String> items = targetLabels;
@@ -240,7 +272,7 @@ final class AboutView {
         JsonArray in = new JsonArray();
         for (About.In e : m.in()) {
             JsonObject jo = new JsonObject();
-            jo.put("label", e.label());
+            jo.put("label", Placeholders.resolve(e.label(), info));
             jo.put("kind", e.kind());
             jo.put("routeId", e.route());
             putTotalAndRate(jo, info, e.route(), totals);
@@ -337,7 +369,8 @@ final class AboutView {
             }
             result.put("selected", sel);
         }
-        result.put("keys", "v all integrations, Up/Down select a line, Enter 
opens it, +/- zoom, Left/Right move, 4 Timeline");
+        result.put("keys", "v all integrations, Up/Down select a line, Enter 
opens it, +/- zoom, Left/Right move, 4 Timeline,"
+                           + (readme != null ? " r the README," : "") + " s 
the summary");
         return result;
     }
 
@@ -512,7 +545,7 @@ final class AboutView {
             int tall = routes == 0 ? 1 : Math.max(1, Math.min(MAX_STRIP_ROWS, 
1 + (area.height() - used - 2) / routes));
             strips = routeStrips(info, width - 4, spaced, tall);
         }
-        select(columns, m);
+        select(columns, m, info);
         String name = " " + (info.name != null ? info.name : info.pid) + " ";
         if (roomy) {
             renderRoomy(frame, area, info, name, head, columns, status, 
strips);
@@ -606,7 +639,7 @@ final class AboutView {
             head.add(Line.from(Span.styled(" " + pFrom, Theme.muted())));
         } else if (o != null) {
             head.add(Line.from(Span.styled(" No description yet: a comment at 
the top of " + mainFile(o)
-                                           + " says what it does, or ask the 
AI (Shift+F8 on a route)",
+                                           + " or a README says what it does, 
or ask the AI (Shift+F8 on a route)",
                     Theme.muted())));
         } else {
             head.add(Line.from(Span.styled(error != null ? " " + error : " 
Reading the sources…", Theme.muted())));
@@ -615,12 +648,12 @@ final class AboutView {
     }
 
     /** The lines the cursor goes over, down In, then Routes, then Out; the 
line it is on in the selection colour. */
-    private void select(Columns c, About.Model m) {
+    private void select(Columns c, About.Model m, IntegrationInfo info) {
         List<Target> all = new ArrayList<>();
         List<String> labels = new ArrayList<>();
         List<String> inLabels = new ArrayList<>();
         if (m != null) {
-            m.in().stream().filter(e -> e.route() != null).forEach(e -> 
inLabels.add(e.label()));
+            m.in().stream().filter(e -> e.route() != null).forEach(e -> 
inLabels.add(Placeholders.resolve(e.label(), info)));
         }
         String[] names = { "in", "route", "out" };
         List<List<Target>> columns = List.of(c.inTargets(), c.routeTargets(), 
c.outTargets());
@@ -896,8 +929,10 @@ final class AboutView {
         return sb.toString();
     }
 
+    /** The file of the first route, where its comment goes: not a file that 
only declares beans. */
     private static String mainFile(ProjectOverview.Overview o) {
-        return o.files().isEmpty() ? "the main file" : o.files().get(0);
+        return o.flows().stream().map(ProjectRoutes.Route::file).filter(f -> f 
!= null).findFirst()
+                .orElse(o.files().isEmpty() ? "the main file" : 
o.files().get(0));
     }
 
     // ---- In, Routes, Out ----
@@ -953,7 +988,9 @@ final class AboutView {
                 inTargets.add(null);
             }
             Long total = e.route() != null ? totals.get(e.route()) : null;
-            in.add(Line.from(Span.raw(" " + icon(e.kind()) + " "), 
Span.styled(e.label(), Style.EMPTY.bold()),
+            // with the values of its properties: the period or the schedule 
it runs with, its profile applied
+            in.add(Line.from(Span.raw(" " + icon(e.kind()) + " "),
+                    Span.styled(Placeholders.resolve(e.label(), info), 
Style.EMPTY.bold()),
                     Span.styled(total != null ? "  " + total : "", 
Theme.success())));
             inTargets.add(e.route() != null ? new Target(e.route(), null) : 
null);
             // the exchanges of the route it starts over the last minutes, and 
the rate now
@@ -1190,12 +1227,23 @@ final class AboutView {
         return dot >= 0 ? type.substring(dot + 1) : type;
     }
 
-    /** Where to go next. */
-    private static Line next() {
-        return Line.from(Span.styled(" Next     ", Theme.muted()), 
Span.styled(" 4 ", Theme.hintKey()),
-                Span.raw(" what happened, per second  "), Span.styled(" 5 ", 
Theme.hintKey()),
-                Span.raw(" diagram  "), Span.styled(" 2 ", Theme.hintKey()), 
Span.raw(" source  "),
-                Span.styled(" v ", Theme.hintKey()), Span.raw(" all 
integrations"));
+    /** Where to go next: the README and the summary of the project too. */
+    private Line next() {
+        List<Span> spans = new ArrayList<>(
+                List.of(Span.styled(" Next     ", Theme.muted()), 
Span.styled(" 4 ", Theme.hintKey()),
+                        Span.raw(" what happened, per second  "), 
Span.styled(" 5 ", Theme.hintKey()),
+                        Span.raw(" diagram  "), Span.styled(" 2 ", 
Theme.hintKey()), Span.raw(" source  ")));
+        if (readme != null) {
+            spans.add(Span.styled(" r ", Theme.hintKey()));
+            spans.add(Span.raw(" readme  "));
+        }
+        if (dir != null) {
+            spans.add(Span.styled(" s ", Theme.hintKey()));
+            spans.add(Span.raw(" summary  "));
+        }
+        spans.add(Span.styled(" v ", Theme.hintKey()));
+        spans.add(Span.raw(" all integrations"));
+        return Line.from(spans);
     }
 
     /**
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 f3f75f40c949..1b82731fe26a 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
@@ -3017,7 +3017,7 @@ class AiPanel {
 
         @Override
         public String selectedProcessName() {
-            return ctx != null ? ctx.selectedName() : null;
+            return ctx != null ? ctx.selectedFullName() : null;
         }
 
         @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDoc.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDoc.java
index b8235df4dd91..e6bf92f7fdbb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDoc.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDoc.java
@@ -48,11 +48,20 @@ final class IntegrationSummaryDoc {
 
     /** The summary of a project for the viewer, or null when there is no 
project or it has no routes. */
     static Doc load(Path dir) {
+        return load(dir, null);
+    }
+
+    /**
+     * The summary of a project for the viewer, named as the integration that 
runs it: an example runs from a directory
+     * of its own (camel-example-7061...), which is no name.
+     */
+    static Doc load(Path dir, String integration) {
         if (dir == null) {
             return null;
         }
         ProjectOverview.Overview overview = ProjectOverview.analyze(dir, 
ProjectOverviewAssist.catalog());
-        String name = dir.getFileName() != null ? dir.getFileName().toString() 
: dir.toString();
+        String dirName = dir.getFileName() != null ? 
dir.getFileName().toString() : dir.toString();
+        String name = integration != null ? integration : dirName;
         Path file = dir.resolve(IntegrationSummary.FILE_NAME);
         if (Files.isRegularFile(file)) {
             try {
@@ -76,7 +85,8 @@ final class IntegrationSummaryDoc {
                         "> Derived from the route sources. Not saved yet: 
/overview in the AI panel explains the"
                                                                                
            + " project and saves it as "
                                                                                
            + IntegrationSummary.FILE_NAME
-                                                                               
            + ".");
+                                                                               
            + ".")
+                .replace("Integration summary: " + dirName, "Integration 
summary: " + name);
         return new Doc("Integration summary · " + name + " (not saved)", 
forViewing(facts));
     }
 
@@ -169,7 +179,11 @@ final class IntegrationSummaryDoc {
         if (ctx == null || ctx.openMarkdownAtCallback == null) {
             return "The document viewer is not available";
         }
-        Doc doc = load(dir);
+        // the name of the integration that runs from the directory
+        IntegrationInfo running = ctx.findSelectedIntegration();
+        String name = running != null && running.directory != null && dir != 
null
+                && Path.of(running.directory).equals(dir) ? running.name : 
null;
+        Doc doc = load(dir, name);
         if (doc == null) {
             return dir == null
                     ? "No source directory for the selected integration"
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvg.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvg.java
index 02687cb5e4a4..bc26cc577db3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvg.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvg.java
@@ -26,14 +26,30 @@ class LoadAvg {
     private double load1 = Double.NaN;
     private double load5 = Double.NaN;
     private double load15 = Double.NaN;
+    private boolean updated;
+
+    /** The averages start with the first value, as Camel's own load of 
inflight exchanges. */
+    LoadAvg() {
+    }
+
+    /**
+     * The averages start at a value, as a load average of the system starts 
at 0: the CPU of a process, whose first
+     * value is the start of its JVM, and is no average of the last 15 minutes.
+     */
+    LoadAvg(double start) {
+        load1 = start;
+        load5 = start;
+        load15 = start;
+    }
 
     synchronized void update(double value) {
+        updated = true;
         load1 = Double.isNaN(load1) ? value : value + EXP_1 * (load1 - value);
         load5 = Double.isNaN(load5) ? value : value + EXP_5 * (load5 - value);
         load15 = Double.isNaN(load15) ? value : value + EXP_15 * (load15 - 
value);
     }
 
     synchronized String format(String fmt) {
-        return Double.isNaN(load1) ? "-" : String.format(Locale.US, fmt, 
load1, load5, load15);
+        return Double.isNaN(load1) || !updated ? "-" : 
String.format(Locale.US, fmt, load1, load5, load15);
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 2486b66e6d78..f6e92c187d95 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1765,7 +1765,7 @@ class McpFacade {
         if (ctx.selectedPid == null) {
             return "Error: no integration selected; give its name or pid";
         }
-        String name = ctx.selectedName();
+        String name = ctx.selectedFullName();
         return switch (action) {
             case "reset-stats", "clear-stats" -> {
                 if (ctx.isInfraSelected()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
index 2ffbfe4da81e..e6838df8cd42 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -453,7 +453,7 @@ class MetricsCollector {
                 long deltaWallNanos = (nowMs - prev[1]) * 1_000_000L;
                 if (deltaWallNanos > 0) {
                     double cpuPct = (double) deltaCpuNanos / deltaWallNanos * 
100.0;
-                    cpuLoadAvg.computeIfAbsent(pid, k -> new 
LoadAvg()).update(Math.max(0, cpuPct));
+                    cpuLoadAvg.computeIfAbsent(pid, k -> new 
LoadAvg(0)).update(Math.max(0, cpuPct));
                 }
             }
             prevCpuSample.put(pid, new long[] { cpuNanos, nowMs });
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 c131107ef264..5ec98f37ebe5 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
@@ -179,6 +179,20 @@ class MonitorContext {
         return confirmActions && !tapeDriven.getAsBoolean();
     }
 
+    /**
+     * The name of the selected integration or infra service as a name to act 
on: a camel command, an answer to a tool.
+     * {@link #selectedName()} is shortened for a title.
+     */
+    String selectedFullName() {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info != null) {
+            return info.name;
+        }
+        InfraInfo infra = findSelectedInfra();
+        return infra != null ? infra.alias : "?";
+    }
+
+    /** The name of the selected integration or infra service for a title, at 
most 20 characters. */
     String selectedName() {
         IntegrationInfo info = findSelectedIntegration();
         if (info != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index aaad494296c4..055c3b20e3d8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -723,7 +723,7 @@ class OverviewTab extends AbstractTab {
                     throughputDisplay = 
TuiHelper.throughputPerMinute(throughputDisplay);
                 }
                 String timingCol;
-                if (hasPercentiles && info.p50Time >= 0) {
+                if (hasPercentiles && info.p50Time >= 0 && info.exchangesTotal 
> 0) {
                     timingCol = formatDurationMs(info.p50Time) + "/" + 
formatDurationMs(info.p95Time) + "/"
                                 + formatDurationMs(info.p99Time);
                 } else if (info.exchangesTotal > 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Placeholders.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Placeholders.java
new file mode 100644
index 000000000000..5b4c68bacb26
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Placeholders.java
@@ -0,0 +1,59 @@
+/*
+ * 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.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The property placeholders of a label read from the sources ({@code 
timer:welcome (period={{welcome.period}})}) with
+ * the values the running integration has for them, its profile applied: the 
properties of its status. A placeholder it
+ * has no value for stays as written, or takes its default ({@code 
{{key:default}}}).
+ */
+final class Placeholders {
+
+    private static final Pattern PLACEHOLDER = 
Pattern.compile("\\{\\{([^{}:]+)(?::([^{}]*))?}}");
+
+    private Placeholders() {
+    }
+
+    static String resolve(String text, IntegrationInfo info) {
+        if (text == null || !text.contains("{{") || info == null) {
+            return text;
+        }
+        Matcher m = PLACEHOLDER.matcher(text);
+        StringBuilder sb = new StringBuilder();
+        while (m.find()) {
+            String value = value(info, m.group(1).strip());
+            if (value == null) {
+                value = m.group(2) != null ? m.group(2) : m.group();
+            }
+            m.appendReplacement(sb, Matcher.quoteReplacement(value));
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    private static String value(IntegrationInfo info, String key) {
+        for (ConfigurationTab.ConfigProperty p : info.configProperties) {
+            if (key.equals(p.key)) {
+                return p.value;
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteDiagramZoom.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteDiagramZoom.java
index b26d0965f37a..acfca22415fa 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteDiagramZoom.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteDiagramZoom.java
@@ -116,8 +116,12 @@ final class RouteDiagramZoom {
         return zoom;
     }
 
-    /** Keeps the density or fit, and the mini panels, for the next start; 
file work, so not on the UI thread. */
-    void persist() {
+    /**
+     * Keeps the density or fit, and the mini panels, for the next start; file 
work, so not on the UI thread. One at a
+     * time: two keys pressed quickly save on two threads, and the one that 
runs last writes what is now, not what was
+     * when its key was pressed.
+     */
+    synchronized void persist() {
         try {
             TuiUserConfig.write(PROP_ROUTE_ZOOM, setting());
             TuiUserConfig.write(PROP_DIAGRAM_PANELS, panels ? null : "false");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index 4c7c20c9eb0c..d74475c66d2a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -524,7 +524,7 @@ class ShellPanel {
 
             // Propagate TUI's selected integration so ask auto-targets it
             if (ctx != null && ctx.selectedPid != null) {
-                EnvironmentHelper.setSelectedProcess(ctx.selectedName());
+                EnvironmentHelper.setSelectedProcess(ctx.selectedFullName());
             }
 
             try (Shell shell = buildShell(terminal, registry)) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 2ea670c691f2..e1542669660e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -16,9 +16,12 @@ What an integration you have not seen before does, and what 
has
 happened to it, with no AI:
 
 - **The header** — the Camel version, runtime, files, routes and uptime,
-  and its purpose: the comment at the top of its main file (a YAML `#`
+  and its purpose: the comment at the top of a route file (a YAML `#`
   comment, a Javadoc or `//` comment in Java, an XML comment), else the
-  descriptions of its routes.
+  first paragraph of its README, else the descriptions of its routes,
+  else the overview an AI wrote in its `camel-summary.md`. A timer or a
+  cron of In shows the period or schedule it runs with, from its
+  properties with its profile applied.
 - **In, Routes, Out** — what starts it (timers, HTTP, consumers), its
   routes (a route called with `direct:` sits under its caller with `↳`,
   one handed off with `seda:` with `⇢`) and what it talks to, from its
@@ -37,7 +40,7 @@ happened to it, with no AI:
   were none in the last minute, with the earlier ones dimmed (Last 5m and
   What happened still show them).
 - **Next** — the keys that lead deeper: **4** the Timeline, **5** the
-  Diagram, **2** the source.
+  Diagram, **2** the source, **r** the README, **s** the summary.
 - **What happened** — a strip per route over the last 5 minutes (as wide
   as the screen allows, and up to two rows high on a tall one), coloured
   as on the Timeline: green completed, yellow recovered by redelivery,
@@ -57,6 +60,9 @@ Keys of About:
   centres on the last trouble, so you see what the other routes did at
   that moment
 - **Left/Right** — Move a zoomed window back and forward
+- **r** — Read the README of the project
+- **s** — The summary of the project: what an AI wrote about it in
+  `camel-summary.md`, and what its sources say
 - **v** — All integrations
 
 `camel get about` shows the same from the command line. An AI reads it
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
index 245204ed0ee2..9689372b56b9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
@@ -376,6 +376,55 @@ class AboutViewTest {
         });
     }
 
+    @Test
+    void theQuickStartRoutesExampleSaysWhatItDoesAndWhenItRuns() throws 
Exception {
+        // quick-start/routes: the bean file comes first, and no file has a 
comment
+        Files.writeString(dir.resolve("beans.yaml"), "- beans:\n    - name: 
greeter\n      type: camel.example.Greeter\n");
+        Files.writeString(dir.resolve("routes.camel.yaml"), """
+                - route:
+                    id: greeting
+                    from:
+                      uri: timer
+                      parameters:
+                        timerName: greet
+                        period: "{{greeter.period}}"
+                      steps:
+                        - log: hello
+                """);
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "routes";
+        info.directory = dir.toString();
+        // the period the integration runs with, from its 
application.properties
+        ConfigurationTab.ConfigProperty period = new 
ConfigurationTab.ConfigProperty();
+        period.key = "greeter.period";
+        period.value = "2000";
+        info.configProperties.add(period);
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+
+        AboutView first = new AboutView(ctx);
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(render(first, info, 200, 34))
+                // the file to put the comment in is the one with the route
+                .contains("No description yet: a comment at the top of 
routes.camel.yaml or a README says what it does")
+                .contains("timer:greet (period=2000)")
+                .contains("s  summary").doesNotContain("r  readme"));
+
+        Files.writeString(dir.resolve("README.md"), """
+                # Routes
+
+                The first step from YAML into your own code: a timer route 
calls a Java bean.
+                """);
+        AboutView about = new AboutView(ctx);
+        List<String> opened = new ArrayList<>();
+        ctx.openMarkdownCallback = (title, markdown) -> opened.add(title + ": 
" + markdown.lines().findFirst().orElse(""));
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(render(about, info, 200, 34))
+                .contains("The first step from YAML into your own code: a 
timer route calls a Java bean.")
+                .contains("from README.md").contains("r  readme"));
+        assertThat(about.handleKeyEvent(KeyEvent.ofChar('r'), info)).isTrue();
+        assertThat(opened).containsExactly("routes - README.md: # Routes");
+        assertThat(about.tableJson(info).getString("keys")).contains("r the 
README, s the summary");
+    }
+
     @Test
     void aboutAnIntegrationWithWhatHasHappened() throws Exception {
         Files.writeString(dir.resolve("shop.camel.yaml"), """
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
index c37f3ddce8e4..2843c0e4bced 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
@@ -16,7 +16,9 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.nio.file.Path;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.stream.Stream;
@@ -26,6 +28,11 @@ import org.apache.camel.diagram.RouteDiagramLayoutEngine;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
@@ -34,11 +41,30 @@ import static org.assertj.core.api.Assertions.assertThat;
 /**
  * The route diagram keys the Diagram and Routes tabs share: they behave the 
same in both tabs. The Routes tab used to
  * keep the detail panel open when going back to the topology.
+ * <p>
+ * Isolated because it changes the home directory of the settings.
  */
+@Isolated
 class AbstractDiagramTabTest {
 
-    private final MonitorContext ctx = new MonitorContext(
-            new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of()));
+    private MonitorContext ctx;
+    private String originalHome;
+
+    @BeforeEach
+    void setUp(@TempDir Path home) {
+        // the keys save the zoom and the mini panels in the user's settings: 
not in the settings of who runs the test
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(home.toString());
+        ctx = new MonitorContext(new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of()));
+    }
+
+    @AfterEach
+    void tearDown() throws InterruptedException {
+        // they save in the background: done before the home is back
+        ctx.backgroundExecutor.shutdown();
+        ctx.backgroundExecutor.awaitTermination(10, TimeUnit.SECONDS);
+        CommandLineHelper.useHomeDir(originalHome);
+    }
 
     static Stream<Function<MonitorContext, AbstractDiagramTab>> tabs() {
         return Stream.of(DiagramTab::new, RoutesTab::new);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDocTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDocTest.java
index 35fc9f63da2a..eae42a161289 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDocTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryDocTest.java
@@ -112,6 +112,23 @@ class IntegrationSummaryDocTest {
         assertTrue(IntegrationSummaryDoc.open(ctx, null, null).startsWith("No 
source directory"));
     }
 
+    @Test
+    void namedAsTheIntegrationThatRunsFromTheDirectory() throws Exception {
+        // an example runs from a directory of its own, camel-example-7061..., 
which is no name
+        write(ROUTES);
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "routes";
+        info.directory = project.toString();
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.selectedPid = "1234";
+        List<String> opened = new ArrayList<>();
+        ctx.openMarkdownAtCallback = (title, markdown, heading) -> 
opened.add(title + " | " + markdown.lines().findFirst()
+                .orElse(""));
+        assertNull(IntegrationSummaryDoc.open(ctx, project, null));
+        assertEquals(List.of("Integration summary · routes (not saved) | # 
Integration summary: routes"), opened);
+    }
+
     @Test
     void viewerScrollsToThePendingHeading() {
         StringBuilder md = new StringBuilder("# Top\n\n");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
index 45781a7449b9..343b4e6a11c6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
@@ -40,6 +40,18 @@ class LoadAvgTest {
         assertEquals("10.00/10.00/10.00", result);
     }
 
+    @Test
+    void theCpuOfAJvmThatJustStartedIsNoAverageOfItsLast15Minutes() {
+        // the first second of a JVM: 500% CPU loading classes and compiling
+        LoadAvg cpu = new LoadAvg(0);
+        assertEquals("-", cpu.format("%.1f/%.1f/%.1f"));
+        cpu.update(500.0);
+        String[] parts = cpu.format("%.1f/%.1f/%.1f").split("/");
+        // a second of 500% is 8% of a minute, and much less of 15 minutes
+        assertEquals(8.3, Double.parseDouble(parts[0]), 0.1);
+        assertTrue(Double.parseDouble(parts[2]) < 1.0, parts[2]);
+    }
+
     @Test
     void multipleUpdatesConvergeViaEwma() {
         LoadAvg avg = new LoadAvg();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeControlTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeControlTest.java
index dbd892585af9..30e7ce73df95 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeControlTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeControlTest.java
@@ -52,6 +52,16 @@ class McpFacadeControlTest {
         assertThat(calls).containsExactly("stopProcess [false]");
     }
 
+    @Test
+    void aLongNameIsAnsweredInFull() {
+        // run/properties-and-profiles: the title shortens it, the answer and 
the shell must not
+        integration("properties-and-profiles", "100");
+
+        assertThat(facade.controlIntegration("stop", 
"properties-and-profiles")).isEqualTo("Stopping properties-and-profiles");
+        
assertThat(ctx.selectedFullName()).isEqualTo("properties-and-profiles");
+        assertThat(ctx.selectedName()).isEqualTo("properties-and-pr...");
+    }
+
     @Test
     void aPidWorksLikeAName() {
         integration("orders", "100");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
index 11e31f58c5e7..40775c212117 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
@@ -85,6 +85,26 @@ class OverviewTabRenderTest {
         assertTrue(rendered.contains("TOTAL"), "Should show TOTAL header");
     }
 
+    @Test
+    void noTimesBeforeTheFirstExchange() {
+        // quick-start/rest-api before its first request: the percentiles are 
there, all 0
+        info.state = 5;
+        info.p50Time = 0;
+        info.p95Time = 0;
+        info.p99Time = 0;
+
+        OverviewTab tab = new OverviewTab(ctx, new MetricsCollector(), new 
HashSet<>(), () -> {
+        }).showIntegrations();
+        String row = TuiTestHelper.renderToString(tab, 160, 
30).lines().filter(l -> l.contains("test-app")).findFirst()
+                .orElseThrow();
+        assertFalse(row.contains("0ms/0ms/0ms"), row);
+
+        info.exchangesTotal = 1;
+        row = TuiTestHelper.renderToString(tab, 160, 30).lines().filter(l -> 
l.contains("test-app")).findFirst()
+                .orElseThrow();
+        assertTrue(row.contains("0ms/0ms/0ms"), row);
+    }
+
     @Test
     void theNumbersStandNextToTheNamesOnAWideScreen() {
         info.state = 5;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PlaceholdersTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PlaceholdersTest.java
new file mode 100644
index 000000000000..fa7aea3c9cf0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PlaceholdersTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * run/properties-and-profiles: the timer of the route runs every 
{{welcome.period}}, which application.properties sets;
+ * About shows the value the integration runs with.
+ */
+class PlaceholdersTest {
+
+    @Test
+    void theValuesTheIntegrationRunsWith() {
+        IntegrationInfo info = new IntegrationInfo();
+        add(info, "welcome.period", "3000");
+        add(info, "report.schedule", "0/10 * * * * ?");
+
+        assertThat(Placeholders.resolve("timer:welcome 
(period={{welcome.period}})", info))
+                .isEqualTo("timer:welcome (period=3000)");
+        assertThat(Placeholders.resolve("cron:report (schedule={{ 
report.schedule }})", info))
+                .isEqualTo("cron:report (schedule=0/10 * * * * ?)");
+        // one it has no value for: its default, else as written
+        assertThat(Placeholders.resolve("timer:t (period={{tick:1000}}, 
delay={{start}})", info))
+                .isEqualTo("timer:t (period=1000, delay={{start}})");
+        // a value with a $ or a \\ is taken as it is
+        add(info, "odd", "$1\\x");
+        assertThat(Placeholders.resolve("{{odd}}", info)).isEqualTo("$1\\x");
+        assertThat(Placeholders.resolve("timer:plain", 
info)).isEqualTo("timer:plain");
+        assertThat(Placeholders.resolve(null, info)).isNull();
+    }
+
+    private static void add(IntegrationInfo info, String key, String value) {
+        ConfigurationTab.ConfigProperty p = new 
ConfigurationTab.ConfigProperty();
+        p.key = key;
+        p.value = value;
+        info.configProperties.add(p);
+    }
+}

Reply via email to