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 dc3f83c2cbb1 CAMEL-23912: Add Maven Dependencies TUI tab
dc3f83c2cbb1 is described below
commit dc3f83c2cbb1e141af2ff464df56ff6328bf540b
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 6 10:18:29 2026 +0200
CAMEL-23912: Add Maven Dependencies TUI tab
New "More" tab showing declared Maven dependencies for the selected
integration. Reads from pom.xml (exported Spring Boot/Quarkus apps)
or .camel-jbang-run.properties (JBang mode). Uses sortable table
with GROUP:ARTIFACT and VERSION columns, filter support, and
property resolution for ${...} placeholders.
Also renames DataSource tab to "JDBC DataSource" to resolve mnemonic
collision (both had 'D' shortcut).
CAMEL-23912: Add scope toggle to Classpath and Maven Dependencies tabs
Press f to cycle scope (all/camel/other) to quickly filter by
Camel vs third-party dependencies. Matches the existing scope
pattern used in the Beans tab.
CAMEL-23912: Add on-demand transitive dependency resolution with parent
tracking
CAMEL-23912: Update TabRegistryTest for Maven Dependencies tab
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 5 +-
.../dsl/jbang/core/commands/tui/ClasspathTab.java | 58 +-
.../dsl/jbang/core/commands/tui/DataSourceTab.java | 4 +-
.../core/commands/tui/MavenDependenciesTab.java | 885 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/TabRegistry.java | 10 +-
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../jbang/core/commands/tui/TabRegistryTest.java | 6 +-
7 files changed, 952 insertions(+), 17 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 2e37d0bb2374..63028e631a96 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -574,11 +574,14 @@ public class CamelMonitor extends CamelCommand {
boolean classpathFilterActive = tabRegistry.selectedTabIndex() ==
TAB_MORE
&& tabRegistry.getActiveMoreTab() == tabRegistry.classpathTab()
&& tabRegistry.classpathTab().isFilterInputActive();
+ boolean mavenDepsFilterActive = tabRegistry.selectedTabIndex() ==
TAB_MORE
+ && tabRegistry.getActiveMoreTab() ==
tabRegistry.mavenDependenciesTab()
+ && tabRegistry.mavenDependenciesTab().isFilterInputActive();
boolean sqlInputActive = tabRegistry.selectedTabIndex() == TAB_MORE
&& tabRegistry.getActiveMoreTab() == tabRegistry.sqlQueryTab()
&& tabRegistry.sqlQueryTab().isInputActive();
boolean textEditing = probeEditing || logSearchActive ||
spanFilterActive || beanFilterActive
- || classpathFilterActive || sqlInputActive;
+ || classpathFilterActive || mavenDepsFilterActive ||
sqlInputActive;
if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
runner.quit();
return true;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index 90ad5700604d..d83afd0ccea6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -52,6 +52,8 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
class ClasspathTab extends AbstractTab {
+ private static final String[] SCOPES = { "all", "camel", "other" };
+
private final ListState listState = new ListState();
private final ScrollbarState listScrollState = new ScrollbarState();
private final AtomicBoolean loading = new AtomicBoolean(false);
@@ -60,6 +62,7 @@ class ClasspathTab extends AbstractTab {
private boolean filterInputActive;
private TextInputState filterInputState = new TextInputState("");
private String filterTerm;
+ private int scopeIndex;
private List<JarEntry> allEntries = Collections.emptyList();
private List<JarEntry> filteredEntries = Collections.emptyList();
private String lastPid;
@@ -89,6 +92,7 @@ class ClasspathTab extends AbstractTab {
filteredEntries = Collections.emptyList();
filterTerm = null;
filterInputActive = false;
+ scopeIndex = 0;
lastPid = null;
errorMessage = null;
dataLoaded = false;
@@ -104,6 +108,11 @@ class ClasspathTab extends AbstractTab {
filterInputState = new TextInputState(filterTerm != null ?
filterTerm : "");
return true;
}
+ if (ke.isCharIgnoreCase('f')) {
+ scopeIndex = (scopeIndex + 1) % SCOPES.length;
+ refilter();
+ return true;
+ }
if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
for (int i = 0; i < 20 && listState.selected() != null &&
listState.selected() > 0; i++) {
listState.selectPrevious();
@@ -222,9 +231,22 @@ class ClasspathTab extends AbstractTab {
items.add(ListItem.from(" No classpath entries
found").style(Style.EMPTY.dim()));
}
- String title = filterTerm != null
- ? String.format(" Classpath [%d/%d] filter:\"%s\" ",
filteredEntries.size(), allEntries.size(), filterTerm)
- : String.format(" Classpath [%d] ", filteredEntries.size());
+ String scope = SCOPES[scopeIndex];
+ StringBuilder sb = new StringBuilder(" Classpath [");
+ if (filterTerm != null || !"all".equals(scope)) {
+
sb.append(filteredEntries.size()).append('/').append(allEntries.size());
+ } else {
+ sb.append(filteredEntries.size());
+ }
+ sb.append(']');
+ if (!"all".equals(scope)) {
+ sb.append(" scope:").append(scope);
+ }
+ if (filterTerm != null) {
+ sb.append(" filter:\"").append(filterTerm).append('"');
+ }
+ sb.append(' ');
+ String title = sb.toString();
ListWidget list = ListWidget.builder()
.items(items.toArray(ListItem[]::new))
@@ -257,6 +279,7 @@ class ClasspathTab extends AbstractTab {
return;
}
hint(spans, "Esc", filterTerm != null ? "clear" : "back");
+ hint(spans, "f", "scope [" + SCOPES[scopeIndex] + "]");
if (filterTerm != null) {
spans.add(Span.styled(" /", Style.EMPTY.fg(Color.YELLOW).bold()));
spans.add(Span.raw("\"" + filterTerm + "\" "));
@@ -341,10 +364,18 @@ class ClasspathTab extends AbstractTab {
private void refilter() {
List<JarEntry> result = new ArrayList<>();
String ft = filterTerm != null ? filterTerm.toLowerCase() : null;
+ String scope = SCOPES[scopeIndex];
for (JarEntry entry : allEntries) {
- if (ft == null || entry.display().toLowerCase().contains(ft)) {
- result.add(entry);
+ if ("camel".equals(scope) && !entry.isCamel()) {
+ continue;
+ }
+ if ("other".equals(scope) && entry.isCamel()) {
+ continue;
}
+ if (ft != null && !entry.display().toLowerCase().contains(ft)) {
+ continue;
+ }
+ result.add(entry);
}
filteredEntries = result;
listState.select(filteredEntries.isEmpty() ? null : 0);
@@ -454,16 +485,22 @@ class ClasspathTab extends AbstractTab {
org.slf4j:slf4j-api 2.0.16
```
+ ## Scope
+
+ Press `f` to cycle the scope filter:
+
+ - **all** — show all classpath entries (default)
+ - **camel** — show only Apache Camel JARs
+ - **other** — show only non-Camel (third-party) JARs
+
+ The active scope is shown in the footer and title bar.
+
## Filter
Press `/` to open the filter input. Type a search term and
press
`Enter` to filter the classpath by substring match. For
example,
type `kafka` to find all Kafka-related JARs, or `jackson` to
find
- Jackson dependencies.
-
- - `/` — open filter input
- - `Enter` — apply filter
- - `Esc` — clear filter
+ Jackson dependencies. The scope and text filter work together.
## When To Use
@@ -476,6 +513,7 @@ class ClasspathTab extends AbstractTab {
- `Up/Down` — navigate entries
- `PgUp/PgDn` — scroll by page
+ - `f` — cycle scope (all, camel, other)
- `/` — open filter
- `Esc` — clear filter or back
""";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
index 8600f6ef787d..726dd6aca8e5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
@@ -176,7 +176,7 @@ class DataSourceTab extends AbstractTableTab {
sorted.sort(this::sortDataSource);
List<String> items = sorted.stream().map(d -> d.name != null ? d.name
: "").toList();
Integer sel = tableState.selected();
- return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "DataSource");
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "JDBC DataSource");
}
@Override
@@ -236,7 +236,7 @@ class DataSourceTab extends AbstractTableTab {
return null;
}
JsonObject result = new JsonObject();
- result.put("tab", "DataSource");
+ result.put("tab", "JDBC DataSource");
JsonArray rows = new JsonArray();
for (DataSourceInfo di : info.dataSources) {
JsonObject row = new JsonObject();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
new file mode 100644
index 000000000000..b1fb11836596
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
@@ -0,0 +1,885 @@
+/*
+ * 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.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.dsl.jbang.core.common.XmlHelper;
+import org.apache.camel.tooling.maven.MavenArtifact;
+import org.apache.camel.tooling.maven.MavenDownloaderImpl;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+/**
+ * TUI tab showing declared Maven dependencies for the selected integration.
Reads from pom.xml (for exported Spring
+ * Boot / Quarkus apps) or from .camel-jbang/camel-jbang-run.properties (for
JBang mode). Unlike the Classpath tab, this
+ * filters out internal infrastructure JARs and shows only the dependencies
the user's application actually declares.
+ */
+class MavenDependenciesTab extends AbstractTableTab {
+
+ private static final String[] SCOPES = { "all", "camel", "other" };
+
+ private final AtomicBoolean loading = new AtomicBoolean(false);
+
+ private boolean filterInputActive;
+ private TextInputState filterInputState = new TextInputState("");
+ private String filterTerm;
+ private int scopeIndex;
+ private List<DepEntry> allEntries = Collections.emptyList();
+ private List<DepEntry> filteredEntries = Collections.emptyList();
+ private String lastPid;
+ private String errorMessage;
+ private boolean dataLoaded;
+ private String dataSource;
+ private boolean transitiveMode;
+ private boolean transitiveLoading;
+ private boolean transitiveLoaded;
+ private List<DepEntry> transitiveEntries = Collections.emptyList();
+
+ MavenDependenciesTab(MonitorContext ctx) {
+ super(ctx, "artifact", "version");
+ }
+
+ @Override
+ public void onTabSelected() {
+ String pid = ctx.selectedPid;
+ if (pid != null && !pid.equals(lastPid)) {
+ lastPid = pid;
+ allEntries = Collections.emptyList();
+ dataLoaded = false;
+ }
+ if (!dataLoaded) {
+ loadDependencies();
+ }
+ }
+
+ @Override
+ public void onIntegrationChanged() {
+ allEntries = Collections.emptyList();
+ filteredEntries = Collections.emptyList();
+ filterTerm = null;
+ filterInputActive = false;
+ scopeIndex = 0;
+ lastPid = null;
+ errorMessage = null;
+ dataLoaded = false;
+ dataSource = null;
+ transitiveMode = false;
+ transitiveLoading = false;
+ transitiveLoaded = false;
+ transitiveEntries = Collections.emptyList();
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (filterInputActive) {
+ return handleFilterInput(ke);
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ @Override
+ protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isChar('/')) {
+ filterInputActive = true;
+ filterInputState = new TextInputState(filterTerm != null ?
filterTerm : "");
+ return true;
+ }
+ if (ke.isCharIgnoreCase('f')) {
+ scopeIndex = (scopeIndex + 1) % SCOPES.length;
+ refilter();
+ return true;
+ }
+ if (ke.isCharIgnoreCase('t')) {
+ if (!transitiveLoaded && !transitiveLoading) {
+ resolveTransitives();
+ } else {
+ transitiveMode = !transitiveMode;
+ refilter();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handleFilterInput(KeyEvent ke) {
+ if (ke.isKey(KeyCode.ESCAPE)) {
+ filterInputActive = false;
+ return true;
+ }
+ if (ke.isConfirm()) {
+ String text = filterInputState.text().trim();
+ filterTerm = text.isEmpty() ? null : text;
+ filterInputActive = false;
+ refilter();
+ return true;
+ }
+ FormHelper.handleTextInput(ke, filterInputState);
+ return true;
+ }
+
+ boolean isFilterInputActive() {
+ return filterInputActive;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ if (filterTerm != null) {
+ filterTerm = null;
+ refilter();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected int getRowCount() {
+ return filteredEntries.size();
+ }
+
+ @Override
+ public void render(Frame frame, Rect area) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ renderNoSelection(frame, area);
+ return;
+ }
+
+ if (loading.get() && allEntries.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(" Loading
dependencies...", Style.EMPTY.dim()))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Maven Dependencies ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ if (errorMessage != null && allEntries.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(
+ Span.styled(" " + errorMessage,
Style.EMPTY.fg(Color.LIGHT_RED)))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Maven Dependencies ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ renderContent(frame, area, info);
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ List<DepEntry> sorted = new ArrayList<>(filteredEntries);
+ sorted.sort(this::sortDep);
+
+ List<Row> rows = new ArrayList<>();
+ for (DepEntry entry : sorted) {
+ String artifact = entry.groupId() + ":" + entry.artifactId();
+ String ver = entry.version() != null ? entry.version() : "";
+ Style style = entry.transitive() ? Style.EMPTY.dim() : Style.EMPTY;
+ if (transitiveMode) {
+ String via = entry.parent() != null ?
shortArtifact(entry.parent()) : "";
+ rows.add(Row.from(
+ Cell.from(Span.styled(" " + artifact, style)),
+ Cell.from(Span.styled(ver, style)),
+ Cell.from(Span.styled(via, Style.EMPTY.dim()))));
+ } else {
+ rows.add(Row.from(
+ Cell.from(Span.styled(" " + artifact, style)),
+ Cell.from(Span.styled(ver, style))));
+ }
+ }
+
+ if (rows.isEmpty() && dataLoaded) {
+ rows.add(emptyRow("No Maven dependency entries found", 2));
+ }
+
+ String scope = SCOPES[scopeIndex];
+ int totalCount = transitiveMode ? combinedEntries().size() :
allEntries.size();
+ boolean filtered = filterTerm != null || !"all".equals(scope);
+ StringBuilder title = new StringBuilder(" Maven Dependencies ");
+ if (transitiveMode) {
+ title.append("(+transitive) ");
+ }
+ title.append('[');
+ if (filtered) {
+
title.append(filteredEntries.size()).append('/').append(totalCount);
+ } else {
+ title.append(filteredEntries.size());
+ }
+ title.append(']');
+ if (!"all".equals(scope)) {
+ title.append(" scope:").append(scope);
+ }
+ if (filterTerm != null) {
+ title.append(" filter:\"").append(filterTerm).append('"');
+ }
+ if (dataSource != null) {
+ title.append(" (").append(dataSource).append(')');
+ }
+ title.append(' ');
+
+ Table.Builder tableBuilder = Table.builder()
+ .rows(rows)
+ .highlightStyle(Theme.selectionBg())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(title.toString()).build());
+
+ if (transitiveMode) {
+ tableBuilder
+ .header(Row.from(
+ Cell.from(Span.styled(" " +
sortLabel("GROUP:ARTIFACT", "artifact"), sortStyle("artifact"))),
+ Cell.from(Span.styled(sortLabel("VERSION",
"version"), sortStyle("version"))),
+ Cell.from(Span.styled("VIA", Style.EMPTY.bold()))))
+ .widths(
+ Constraint.fill(),
+ Constraint.length(20),
+ Constraint.length(30));
+ } else {
+ tableBuilder
+ .header(Row.from(
+ Cell.from(Span.styled(" " +
sortLabel("GROUP:ARTIFACT", "artifact"), sortStyle("artifact"))),
+ Cell.from(Span.styled(sortLabel("VERSION",
"version"), sortStyle("version")))))
+ .widths(
+ Constraint.fill(),
+ Constraint.length(20));
+ }
+
+ Table table = tableBuilder.build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, sorted.size());
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ if (filterInputActive) {
+ spans.add(Span.styled(" /", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.raw(filterInputState.text() + "█ "));
+ hint(spans, "Enter", "filter");
+ hintLast(spans, "Esc", "cancel");
+ return;
+ }
+ hint(spans, "Esc", filterTerm != null ? "clear" : "back");
+ hint(spans, "s", "sort");
+ hint(spans, "f", "scope [" + SCOPES[scopeIndex] + "]");
+ if (transitiveLoading) {
+ hint(spans, "t", "resolving...");
+ } else {
+ hint(spans, "t", transitiveMode ? "direct" : "transitive");
+ }
+ if (filterTerm != null) {
+ spans.add(Span.styled(" /", Style.EMPTY.fg(Color.YELLOW).bold()));
+ spans.add(Span.raw("\"" + filterTerm + "\" "));
+ } else {
+ hint(spans, "/", "filter");
+ }
+ hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
+ }
+
+ private int sortDep(DepEntry a, DepEntry b) {
+ int result = switch (sort) {
+ case "version" -> {
+ String va = a.version() != null ? a.version() : "";
+ String vb = b.version() != null ? b.version() : "";
+ yield va.compareToIgnoreCase(vb);
+ }
+ default -> { // "artifact"
+ yield a.display().compareToIgnoreCase(b.display());
+ }
+ };
+ return sortReversed ? -result : result;
+ }
+
+ private void loadDependencies() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || ctx.runner == null) {
+ return;
+ }
+ if (!loading.compareAndSet(false, true)) {
+ return;
+ }
+
+ ctx.runner.scheduler().execute(() -> {
+ try {
+ List<Path> candidates = new ArrayList<>();
+ Path sourceDir = FilesBrowser.resolveSourceDirectory(info);
+ if (sourceDir != null) {
+ candidates.add(sourceDir);
+ }
+ if (info.directory != null && !info.directory.isEmpty()) {
+ Path infoDir = Path.of(info.directory);
+ if (!candidates.contains(infoDir)) {
+ candidates.add(infoDir);
+ }
+ }
+ if (candidates.isEmpty()) {
+ applyResult(Collections.emptyList(), null, "Cannot
determine integration directory");
+ return;
+ }
+
+ boolean jbangMode = "JBang".equals(info.platform) ||
"Camel".equals(info.platform);
+
+ if (jbangMode) {
+ for (Path dir : candidates) {
+ Path propsFile =
dir.resolve(".camel-jbang").resolve("camel-jbang-run.properties");
+ if (Files.exists(propsFile)) {
+ List<DepEntry> deps =
loadFromRunProperties(propsFile);
+ applyResult(deps, "jbang", deps.isEmpty() ? "No
dependencies in run properties" : null);
+ return;
+ }
+ }
+ }
+
+ for (Path dir : candidates) {
+ Path pomFile = dir.resolve("pom.xml");
+ if (Files.exists(pomFile)) {
+ List<DepEntry> deps = loadFromPomXml(pomFile);
+ applyResult(deps, "pom.xml", deps.isEmpty() ? "No
compile dependencies in pom.xml" : null);
+ return;
+ }
+ }
+
+ if (!jbangMode) {
+ for (Path dir : candidates) {
+ Path propsFile =
dir.resolve(".camel-jbang").resolve("camel-jbang-run.properties");
+ if (Files.exists(propsFile)) {
+ List<DepEntry> deps =
loadFromRunProperties(propsFile);
+ applyResult(deps, "jbang", deps.isEmpty() ? "No
dependencies in run properties" : null);
+ return;
+ }
+ }
+ }
+
+ applyResult(Collections.emptyList(), null, "No dependency
information found");
+ } catch (Exception e) {
+ applyResult(Collections.emptyList(), null, "Error: " +
e.getMessage());
+ } finally {
+ loading.set(false);
+ }
+ });
+ }
+
+ private void resolveTransitives() {
+ if (allEntries.isEmpty() || ctx.runner == null) {
+ return;
+ }
+ transitiveLoading = true;
+ ctx.runner.scheduler().execute(() -> {
+ try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+ downloader.build();
+
+ Set<String> directKeys = new HashSet<>();
+ for (DepEntry d : allEntries) {
+ directKeys.add(d.groupId() + ":" + d.artifactId());
+ }
+
+ boolean hasSnapshot = allEntries.stream()
+ .anyMatch(d -> d.version() != null &&
d.version().contains("SNAPSHOT"));
+
+ Map<String, DepEntry> transitiveMap = new HashMap<>();
+ for (DepEntry direct : allEntries) {
+ if (direct.version() == null) {
+ continue;
+ }
+ String gav = direct.groupId() + ":" + direct.artifactId()
+ ":" + direct.version();
+ String parentKey = direct.groupId() + ":" +
direct.artifactId();
+ try {
+ List<MavenArtifact> resolved =
downloader.resolveArtifacts(
+ List.of(gav), Set.of(), true, hasSnapshot);
+ for (MavenArtifact ma : resolved) {
+ MavenGav g = ma.getGav();
+ if (skipArtifact(g.getGroupId(),
g.getArtifactId())) {
+ continue;
+ }
+ String key = g.getGroupId() + ":" +
g.getArtifactId();
+ if (!directKeys.contains(key) &&
!transitiveMap.containsKey(key)) {
+ transitiveMap.put(key, new DepEntry(
+ g.getGroupId(), g.getArtifactId(),
g.getVersion(), true, parentKey));
+ }
+ }
+ } catch (Exception e) {
+ // skip this dependency
+ }
+ }
+
+ List<DepEntry> entries = new
ArrayList<>(transitiveMap.values());
+ entries.sort(Comparator.comparing(DepEntry::display,
String.CASE_INSENSITIVE_ORDER));
+ applyTransitiveResult(entries);
+ } catch (Exception e) {
+ applyTransitiveResult(Collections.emptyList());
+ }
+ });
+ }
+
+ private void applyTransitiveResult(List<DepEntry> entries) {
+ if (ctx.runner == null) {
+ return;
+ }
+ ctx.runner.runOnRenderThread(() -> {
+ transitiveEntries = entries;
+ transitiveLoading = false;
+ transitiveLoaded = true;
+ transitiveMode = true;
+ refilter();
+ });
+ }
+
+ private List<DepEntry> loadFromPomXml(Path pomFile) {
+ try {
+ DocumentBuilderFactory dbf =
XmlHelper.createDocumentBuilderFactory();
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ Document dom;
+ try (InputStream is = Files.newInputStream(pomFile)) {
+ dom = db.parse(is);
+ }
+
+ Map<String, String> properties = new HashMap<>();
+ NodeList propsList = dom.getElementsByTagName("properties");
+ if (propsList.getLength() > 0) {
+ Element propsEl = (Element) propsList.item(0);
+ for (int i = 0; i < propsEl.getChildNodes().getLength(); i++) {
+ if (propsEl.getChildNodes().item(i) instanceof Element
prop) {
+ properties.put(prop.getTagName(),
prop.getTextContent().trim());
+ }
+ }
+ }
+
+ NodeList nl = dom.getElementsByTagName("dependency");
+ List<DepEntry> deps = new ArrayList<>();
+ String camelVersion = null;
+ String springBootVersion = null;
+ String quarkusVersion = null;
+
+ for (int i = 0; i < nl.getLength(); i++) {
+ Element node = (Element) nl.item(i);
+
+ String p = node.getParentNode().getNodeName();
+ String p2 = node.getParentNode().getParentNode().getNodeName();
+ boolean accept = ("dependencyManagement".equals(p2) ||
"project".equals(p2))
+ && "dependencies".equals(p);
+ if (!accept) {
+ continue;
+ }
+
+ String g =
node.getElementsByTagName("groupId").item(0).getTextContent();
+ String a =
node.getElementsByTagName("artifactId").item(0).getTextContent();
+ String v = null;
+ NodeList vl = node.getElementsByTagName("version");
+ if (vl.getLength() > 0) {
+ v = vl.item(0).getTextContent();
+ }
+
+ v = resolveProperty(v, properties);
+
+ if ("org.apache.camel".equals(g) && "camel-bom".equals(a)) {
+ camelVersion = v;
+ continue;
+ }
+ if ("org.apache.camel.springboot".equals(g) &&
"camel-spring-boot-bom".equals(a)) {
+ camelVersion = v;
+ continue;
+ }
+ if ("org.springframework.boot".equals(g) &&
"spring-boot-dependencies".equals(a)) {
+ springBootVersion = v;
+ continue;
+ }
+ if (("${quarkus.platform.group-id}".equals(g) ||
"io.quarkus.platform".equals(g))
+ && ("${quarkus.platform.artifact-id}".equals(a) ||
"quarkus-bom".equals(a))) {
+ if ("${quarkus.platform.version}".equals(v)) {
+ NodeList qvl =
dom.getElementsByTagName("quarkus.platform.version");
+ if (qvl.getLength() > 0) {
+ quarkusVersion = qvl.item(0).getTextContent();
+ }
+ } else {
+ quarkusVersion = v;
+ }
+ continue;
+ }
+
+ String scope = null;
+ NodeList sl = node.getElementsByTagName("scope");
+ if (sl.getLength() > 0) {
+ scope = sl.item(0).getTextContent();
+ }
+ if ("test".equals(scope) || "import".equals(scope)) {
+ continue;
+ }
+
+ if (v == null && "org.apache.camel".equals(g)) {
+ v = camelVersion;
+ }
+ if (v == null && "org.apache.camel.springboot".equals(g)) {
+ v = camelVersion;
+ }
+ if (v == null && "org.springframework.boot".equals(g)) {
+ v = springBootVersion;
+ }
+ if (v == null && ("io.quarkus".equals(g) ||
"org.apache.camel.quarkus".equals(g))) {
+ v = quarkusVersion;
+ }
+
+ g = resolveProperty(g, properties);
+ a = resolveProperty(a, properties);
+ v = resolveProperty(v, properties);
+
+ if (skipArtifact(g, a)) {
+ continue;
+ }
+
+ deps.add(new DepEntry(g, a, v));
+ }
+
+ deps.sort(Comparator.comparing(DepEntry::display,
String.CASE_INSENSITIVE_ORDER));
+ return deps;
+ } catch (Exception e) {
+ return Collections.emptyList();
+ }
+ }
+
+ private List<DepEntry> loadFromRunProperties(Path propsFile) {
+ try {
+ List<String> lines = Files.readAllLines(propsFile);
+ List<DepEntry> deps = new ArrayList<>();
+
+ for (String line : lines) {
+ line = line.trim();
+ if (!line.startsWith("dependency=")) {
+ continue;
+ }
+ String value = line.substring("dependency=".length()).trim();
+ value = value.replace("\\:", ":");
+
+ if (value.startsWith("mvn:") || value.startsWith("mvn\\:")) {
+ String gav = value.startsWith("mvn:") ? value.substring(4)
: value.substring(5);
+ MavenGav parsed = MavenGav.parseGav(gav);
+ if (parsed.getGroupId() != null && parsed.getArtifactId()
!= null) {
+ if (!skipArtifact(parsed.getGroupId(),
parsed.getArtifactId())) {
+ deps.add(new DepEntry(parsed.getGroupId(),
parsed.getArtifactId(), parsed.getVersion()));
+ }
+ }
+ }
+ }
+
+ deps.sort(Comparator.comparing(DepEntry::display,
String.CASE_INSENSITIVE_ORDER));
+ return deps;
+ } catch (Exception e) {
+ return Collections.emptyList();
+ }
+ }
+
+ private static String resolveProperty(String value, Map<String, String>
properties) {
+ if (value != null && value.startsWith("${") && value.endsWith("}")) {
+ String key = value.substring(2, value.length() - 1);
+ String resolved = properties.get(key);
+ if (resolved != null) {
+ return resolved;
+ }
+ }
+ return value;
+ }
+
+ private static String shortArtifact(String ga) {
+ if (ga == null) {
+ return "";
+ }
+ int colon = ga.indexOf(':');
+ return colon >= 0 ? ga.substring(colon + 1) : ga;
+ }
+
+ private static boolean skipArtifact(String groupId, String artifactId) {
+ if ("org.fusesource.jansi".equals(groupId)) {
+ return true;
+ }
+ if ("org.apache.logging.log4j".equals(groupId)) {
+ return true;
+ }
+ if ("org.apache.camel".equals(groupId) &&
"camel-kamelet-main".equals(artifactId)) {
+ return true;
+ }
+ if ("org.apache.camel".equals(groupId) &&
"camel-cli-connector".equals(artifactId)) {
+ return true;
+ }
+ return false;
+ }
+
+ private void applyResult(List<DepEntry> parsed, String source, String
error) {
+ if (ctx.runner == null) {
+ return;
+ }
+ ctx.runner.runOnRenderThread(() -> {
+ allEntries = parsed;
+ errorMessage = error;
+ dataSource = source;
+ dataLoaded = true;
+ refilter();
+ });
+ }
+
+ private void refilter() {
+ List<DepEntry> source = transitiveMode
+ ? combinedEntries()
+ : allEntries;
+ List<DepEntry> result = new ArrayList<>();
+ String ft = filterTerm != null ? filterTerm.toLowerCase() : null;
+ String scope = SCOPES[scopeIndex];
+ for (DepEntry entry : source) {
+ if ("camel".equals(scope) && !entry.isCamel()) {
+ continue;
+ }
+ if ("other".equals(scope) && entry.isCamel()) {
+ continue;
+ }
+ if (ft != null && !entry.display().toLowerCase().contains(ft)) {
+ continue;
+ }
+ result.add(entry);
+ }
+ filteredEntries = result;
+ if (!filteredEntries.isEmpty()) {
+ tableState.select(0);
+ }
+ }
+
+ private List<DepEntry> combinedEntries() {
+ Set<String> directKeys = new HashSet<>();
+ for (DepEntry d : allEntries) {
+ directKeys.add(d.groupId() + ":" + d.artifactId());
+ }
+ List<DepEntry> combined = new ArrayList<>(allEntries);
+ for (DepEntry t : transitiveEntries) {
+ if (!directKeys.contains(t.groupId() + ":" + t.artifactId())) {
+ combined.add(t);
+ }
+ }
+ combined.sort(Comparator.comparing(DepEntry::display,
String.CASE_INSENSITIVE_ORDER));
+ return combined;
+ }
+
+ @Override
+ public boolean setFilter(String filter) {
+ filterTerm = filter != null && !filter.isEmpty() ? filter : null;
+ refilter();
+ return true;
+ }
+
+ @Override
+ public boolean setInputValue(String field, String value) {
+ if ("filter".equals(field)) {
+ return setFilter(value);
+ }
+ return false;
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ if (filteredEntries.isEmpty()) {
+ return null;
+ }
+ List<String> items =
filteredEntries.stream().map(DepEntry::display).toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Maven Dependencies");
+ }
+
+ record DepEntry(String groupId, String artifactId, String version, boolean
transitive, String parent) {
+
+ DepEntry(String groupId, String artifactId, String version) {
+ this(groupId, artifactId, version, false, null);
+ }
+
+ String display() {
+ if (version != null) {
+ return groupId + ":" + artifactId + ":" + version;
+ }
+ return groupId + ":" + artifactId;
+ }
+
+ boolean isCamel() {
+ return groupId != null && groupId.startsWith("org.apache.camel");
+ }
+ }
+
+ @Override
+ public String description() {
+ return "Maven dependency entries with filtering";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Maven Dependencies
+
+ The Maven Dependencies tab shows the declared dependencies for
the
+ selected integration, as they would appear in a pom.xml file.
Unlike
+ the Classpath tab (which shows all JARs on the JVM classpath
including
+ internal infrastructure), this tab filters out internal
bootstrap JARs
+ and shows only the dependencies the application actually
declares.
+
+ ## Data Sources
+
+ The tab discovers dependencies from (in order of priority):
+
+ - **pom.xml** — For exported Maven projects (Spring Boot,
Quarkus).
+ Parses compile-scoped dependencies from the project's
pom.xml.
+ - **.camel-jbang/camel-jbang-run.properties** — For Camel CLI
(JBang)
+ mode. Reads the `dependency=mvn:...` lines that list all
declared
+ dependencies.
+
+ The data source is shown in the title bar (e.g., "pom.xml" or
"jbang").
+
+ ## Table Columns
+
+ - **GROUP:ARTIFACT** — Maven coordinate (e.g.,
`org.apache.camel:camel-core`)
+ - **VERSION** — The dependency version (e.g., `4.22.0`)
+
+ Each row shows the Maven coordinate and its version.
+
+ ## Sort
+
+ Press `s` to cycle sort column (artifact, version).
+ Press `S` to reverse sort order.
+
+ ## Scope
+
+ Press `f` to cycle the scope filter:
+
+ - **all** — show all dependencies (default)
+ - **camel** — show only Apache Camel dependencies
+ - **other** — show only non-Camel (third-party) dependencies
+
+ The active scope is shown in the footer and title bar.
+
+ ## Transitive Dependencies
+
+ Press `t` to resolve transitive dependencies on demand. The
first
+ press triggers Maven resolution (shown as "resolving..." in the
+ footer). Once resolved, transitive dependencies appear dimmed
in
+ the table with a **VIA** column showing which direct dependency
+ pulled them in. Subsequent presses toggle between showing all
+ dependencies (direct + transitive) and direct only.
+
+ The title shows "(+transitive)" when transitive mode is active.
+ This is useful for CVE auditing — when a transitive JAR has a
+ known vulnerability, the VIA column tells you which direct
+ dependency to upgrade or exclude.
+
+ ## Filter
+
+ Press `/` to open the filter input. Type a search term and
press
+ `Enter` to filter by substring match. For example, type
`kafka` to
+ find Kafka-related dependencies, or `spring` to find Spring
+ dependencies. The scope and text filter work together.
+
+ ## When To Use
+
+ - **CVE auditing**: Review only the real application
dependencies, not
+ internal infrastructure JARs that leak in via
camel-kamelet-main
+ - **Dependency review**: Understand what your integration
actually
+ depends on
+ - **Version checking**: Verify specific dependency versions
+ - **Transitive analysis**: See the full dependency tree
similar to
+ `mvn dependency:tree` (flat list)
+
+ ## Keys
+
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ - `f` — cycle scope (all, camel, other)
+ - `t` — resolve and toggle transitive dependencies
+ - `/` — open filter
+ - `Esc` — clear filter or back
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ List<DepEntry> entries = filteredEntries;
+ if (entries.isEmpty()) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Maven Dependencies");
+ JsonArray rows = new JsonArray();
+ for (DepEntry d : entries) {
+ JsonObject row = new JsonObject();
+ row.put("groupId", d.groupId());
+ row.put("artifactId", d.artifactId());
+ if (d.version() != null) {
+ row.put("version", d.version());
+ }
+ if (d.transitive()) {
+ row.put("transitive", true);
+ if (d.parent() != null) {
+ row.put("parent", d.parent());
+ }
+ }
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", entries.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ if (dataSource != null) {
+ result.put("dataSource", dataSource);
+ }
+ return result;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 84dc86fed2c1..fbcd5724e4ec 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -80,6 +80,7 @@ class TabRegistry {
private BeansTab beansTab;
private BrowseTab browseTab;
private ClasspathTab classpathTab;
+ private MavenDependenciesTab mavenDependenciesTab;
private CveAuditTab cveAuditTab;
private InflightTab inflightTab;
private MemoryTab memoryTab;
@@ -126,6 +127,7 @@ class TabRegistry {
beansTab = new BeansTab(ctx);
browseTab = new BrowseTab(ctx);
classpathTab = new ClasspathTab(ctx);
+ mavenDependenciesTab = new MavenDependenciesTab(ctx);
cveAuditTab = new CveAuditTab(ctx);
inflightTab = new InflightTab(ctx);
memoryTab = new MemoryTab(ctx, dataService.metrics());
@@ -151,9 +153,10 @@ class TabRegistry {
new MoreTab(TuiIcons.TAB_CONFIGURATION, "Configuration",
"Confi&guration", configurationTab),
new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers",
consumersTab),
new MoreTab(TuiIcons.TAB_CVE_AUDIT, "CVE Audit", "C&VE Audit",
cveAuditTab),
- new MoreTab(TuiIcons.TAB_DATASOURCE, "DataSource",
"&DataSource", dataSourceTab),
new MoreTab(TuiIcons.TAB_HEAP, "Heap Histogram", "&Heap
Histogram", heapHistogramTab),
new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "&Inflight",
inflightTab),
+ new MoreTab(TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC
DataSource", dataSourceTab),
+ new MoreTab(TuiIcons.TAB_MAVEN_DEPENDENCIES, "Maven
Dependencies", "Maven &Dependencies", mavenDependenciesTab),
new MoreTab(TuiIcons.TAB_MEMORY, "Memory", "&Memory",
memoryTab),
new MoreTab(TuiIcons.TAB_MEMORY_LEAK, "Memory Leak", "Memory
Lea&k", memoryLeakTab),
new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "M&etrics",
metricsTab),
@@ -262,6 +265,7 @@ class TabRegistry {
cveAuditTab.onIntegrationChanged();
dataSourceTab.onIntegrationChanged();
heapHistogramTab.onIntegrationChanged();
+ mavenDependenciesTab.onIntegrationChanged();
memoryLeakTab.onIntegrationChanged();
sqlQueryTab.onIntegrationChanged();
sqlTraceTab.onIntegrationChanged();
@@ -332,6 +336,10 @@ class TabRegistry {
return classpathTab;
}
+ MavenDependenciesTab mavenDependenciesTab() {
+ return mavenDependenciesTab;
+ }
+
CveAuditTab cveAuditTab() {
return cveAuditTab;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 0425aea301f4..e46cda174ace 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -131,6 +131,7 @@ final class TuiIcons {
static final String TAB_DATASOURCE = "💿";
static final String TAB_HEAP = MEMORY;
static final String TAB_INFLIGHT = RESET;
+ static final String TAB_MAVEN_DEPENDENCIES = "📜";
static final String TAB_MEMORY = MEMORY;
static final String TAB_MEMORY_LEAK = "💧";
static final String TAB_METRICS = "📈";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 8e5a0757db69..cbe2757d0c96 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,8 +82,8 @@ class TabRegistryTest {
}
@Test
- void moreTabsHasNineteenEntries() {
- assertEquals(19, registry.moreTabs().size());
+ void moreTabsHasTwentyEntries() {
+ assertEquals(20, registry.moreTabs().size());
}
@Test
@@ -109,7 +109,7 @@ class TabRegistryTest {
// MORE_SHORTCUTS array carried before the MoreTab refactor. A label
edit that repoints a key must fail here.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
assertEquals(
- List.of('B', 'W', 'C', 'A', 'G', 'N', 'V', 'D', 'H', 'I', 'M',
'K', 'E', 'Q', 'R', 'O', 'P', 'S', 'T'),
+ List.of('B', 'W', 'C', 'A', 'G', 'N', 'V', 'H', 'I', 'J', 'D',
'M', 'K', 'E', 'Q', 'R', 'O', 'P', 'S', 'T'),
shortcuts, "More tab shortcut letters must match the
historical sequence");
}