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 bda1d565679e CAMEL-24237: TUI Catalog tab doc viewer and EIP scope
bda1d565679e is described below

commit bda1d565679e23a589afb3fab86f1abfe7d712bc
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Jul 23 07:48:10 2026 +0200

    CAMEL-24237: TUI Catalog tab doc viewer and EIP scope
    
    Add documentation viewer (d key), options viewer (o key), and table of
    contents popup (t key) to the TUI Catalog tab. Extend EIP scope with
    docs for doTry, a2aSubTask, tokenizer, pausable, and resumable patterns.
    Add doc/options switching, type-ahead TOC navigation, scroll clamping,
    and MCP tool support for catalog docs with includeDoc parameter.
    
    Closes #25018
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../org/apache/camel/catalog/docs.properties       |   5 +
 .../apache/camel/catalog/docs/a2aSubTask-eip.adoc  | 100 +++++
 .../org/apache/camel/catalog/docs/doTry-eip.adoc   | 246 +++++++++++
 .../apache/camel/catalog/docs/pausable-eip.adoc    |  54 +++
 .../apache/camel/catalog/docs/resumable-eip.adoc   |  56 +++
 .../apache/camel/catalog/docs/tokenizer-eip.adoc   | 109 +++++
 .../src/main/docs/modules/eips/nav.adoc            |   5 +
 .../docs/modules/eips/pages/a2aSubTask-eip.adoc    | 100 +++++
 .../main/docs/modules/eips/pages/doTry-eip.adoc    | 246 +++++++++++
 .../main/docs/modules/eips/pages/pausable-eip.adoc |  54 +++
 .../docs/modules/eips/pages/resumable-eip.adoc     |  56 +++
 .../docs/modules/eips/pages/tokenizer-eip.adoc     | 109 +++++
 .../camel/cli/connector/LocalCliConnector.java     |   6 +-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  58 ++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   3 +
 .../dsl/jbang/core/commands/tui/CatalogTab.java    | 186 ++++++++-
 .../jbang/core/commands/tui/DocViewerPopup.java    | 153 ++++++-
 .../jbang/core/commands/tui/MonitorContext.java    |   9 +
 .../core/commands/tui/OptionsViewerPopup.java      | 462 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/TocPopup.java      | 288 +++++++++++++
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  84 +++-
 21 files changed, 2347 insertions(+), 42 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
index 518dd4f70586..86aa824fbf01 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
@@ -1,6 +1,7 @@
 a2a-component
 a2a-consumer
 a2a-producer
+a2aSubTask-eip
 activemq-component
 activemq6-component
 aggregate-eip
@@ -152,6 +153,7 @@ disruptor-component
 disruptor-vm-component
 djl-component
 dns-component
+doTry-eip
 docker-component
 docling-async-processing
 docling-batch-processing
@@ -462,6 +464,7 @@ optaplanner-component
 paho-component
 paho-mqtt5-component
 parquetAvro-dataformat
+pausable-eip
 pdf-component
 pg-replication-slot-component
 pgevent-component
@@ -519,6 +522,7 @@ resourceresolver-github
 rest-api-component
 rest-component
 rest-openapi-component
+resumable-eip
 resume-strategies
 return-address
 robotframework-component
@@ -638,6 +642,7 @@ timer-component
 to-eip
 toD-eip
 tokenize-language
+tokenizer-eip
 topicLoadBalancer-eip
 tracing
 transactional-client
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/a2aSubTask-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/a2aSubTask-eip.adoc
new file mode 100644
index 000000000000..00e9a1eeb39c
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/a2aSubTask-eip.adoc
@@ -0,0 +1,100 @@
+= A2A Sub Task EIP
+:doctitle: A2A Sub Task
+:shortname: a2aSubTask
+:description: Groups route steps and emits A2A (Agent-to-Agent) protocol 
progress events before, after, or when the grouped work fails
+:since: 4.21
+:supportlevel: Stable
+:tabs-sync-option:
+
+The A2A Sub Task EIP groups route steps and emits 
https://a2a-protocol.org/latest/specification/[A2A protocol] progress events
+before, after, or when the grouped work fails.
+
+This is useful for xref:ROOT:a2a-component.adoc[A2A] consumer routes where you 
want to report scoped progress updates
+to the calling agent as the route processes different stages of work.
+
+The `emitBefore`, `emitAfter`, and `emitOnError` fields are optional and are 
evaluated as
+xref:languages:simple-language.adoc[Simple] expressions against the current 
Exchange.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Example
+
+In this example, the route groups two stages of work and emits progress events 
for each:
+
+[tabs]
+====
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: a2a:classpath:agent-card.json
+      steps:
+        - a2aSubTask:
+            id: search-docs-progress
+            emitBefore: "Searching docs..."
+            emitAfter: "Docs found: ${body.size()}"
+            emitOnError: "Error searching docs: ${exception.message}"
+            steps:
+              - to:
+                  uri: "elasticsearch:docs?operation=Search"
+        - a2aSubTask:
+            id: draft-answer-progress
+            emitBefore: "Drafting answer..."
+            emitAfter: "Answer drafted: ${body}"
+            emitOnError: "Error drafting answer: ${exception.message}"
+            steps:
+              - bean:
+                  ref: answerDraftingService
+                  method: draft
+        - setBody:
+            simple: "Final answer: ${body}"
+----
+
+Java::
++
+[source,java]
+----
+from("a2a:classpath:agent-card.json")
+    .a2aSubTask()
+        .id("search-docs-progress")
+        .emitBefore("Searching docs...")
+        .emitAfter("Docs found: ${body.size()}")
+        .emitOnError("Error searching docs: ${exception.message}")
+        .to("elasticsearch:docs?operation=Search")
+    .end()
+    .a2aSubTask()
+        .id("draft-answer-progress")
+        .emitBefore("Drafting answer...")
+        .emitAfter("Answer drafted: ${body}")
+        .emitOnError("Error drafting answer: ${exception.message}")
+        .bean(answerDraftingService, "draft")
+    .end()
+    .setBody(simple("Final answer: ${body}"));
+----
+====
+
+== Behavior
+
+* The nested `steps` behave like normal Camel route steps.
+* If the nested steps fail, `emitOnError` can access the original exception 
via `${exception.message}`,
+  and the original exception continues to propagate through the route.
+* Failures while evaluating the `emitBefore`, `emitAfter`, or `emitOnError` 
expressions are route failures.
+* Failures while storing or notifying the resulting A2A progress event are 
best-effort: they are logged
+  at debug level and do not stop nested work, fail an otherwise successful 
exchange, or replace
+  the original nested-step exception.
+* By default, emitting progress outside an active A2A task context is a no-op.
+  Set `failIfNoTaskContext=true` when a route should fail if the current 
Exchange
+  does not carry an active A2A task.
+* Use stable `id` values on long-lived sub-tasks so route tracing and 
route-structure views keep stable node names.
+
+== See Also
+
+* xref:ROOT:a2a-component.adoc[A2A Component]
+* xref:others:a2a-consumer.adoc[A2A Consumer Guide]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/doTry-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/doTry-eip.adoc
new file mode 100644
index 000000000000..0b43a808862f
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/doTry-eip.adoc
@@ -0,0 +1,246 @@
+= Do Try EIP
+:doctitle: Do Try
+:shortname: doTry
+:description: Wraps route steps in a try-catch-finally block for fine-grained 
exception handling within the route
+:since: 
+:supportlevel: Stable
+:tabs-sync-option:
+
+Camel supports the Java equivalent of try, catch, and finally directly in the 
DSL.
+It aims to work like its Java sisters but with more power.
+
+In Camel, we prefix the keywords with `do` to avoid having same keyword as 
Java.
+So we have:
+
+- `doTry`
+- `doCatch`
+- `doFinally`
+- `end` to end the block in Java DSL
+
+[IMPORTANT]
+====
+When using `doTry ... doCatch ... doFinally`, the regular Camel
+xref:manual::error-handler.adoc[Error Handler] is not in use; meaning any
+`onException` or the likes does not trigger.
+
+The reason is that
+`doTry ... doCatch ... doFinally` is in fact its own error handler and mimics 
how try/catch/finally works in Java.
+====
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Using doTry ... doCatch ... doFinally
+
+In the route below we have all of them in action:
+
+[tabs]
+====
+
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .doTry()
+        .process(new ProcessorFail())
+        .to("mock:result")
+    .doCatch(IOException.class, IllegalStateException.class)
+        .to("mock:catch")
+    .doFinally()
+        .to("mock:finally")
+    .end();
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:start"/>
+  <doTry>
+    <process ref="processorFail"/>
+    <to uri="mock:result"/>
+    <doCatch>
+      <exception>java.io.IOException</exception>
+      <exception>java.lang.IllegalStateException</exception>
+      <to uri="mock:catch"/>
+    </doCatch>
+    <doFinally>
+       <to uri="mock:finally"/>
+    </doFinally>
+  </doTry>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:start
+      steps:
+        - doTry:
+            steps:
+              - process:
+                  ref: processorFail
+              - to:
+                  uri: mock:result
+              - doCatch:
+                  exception:
+                    - java.io.IOException
+                    - java.lang.IllegalStateException
+                  steps:
+                    - to:
+                        uri: mock:catch
+              - doFinally:
+                  steps:
+                    - to:
+                        uri: mock:finally
+----
+====
+
+=== Using onWhen with doCatch
+
+You can use xref:manual::predicate.adoc[Predicates] with `doCatch` to make it 
determine at runtime if the block should be triggered or not.
+In this example, we only want to trigger if the caused exception message 
contains the word *Damn*.
+
+[tabs]
+====
+
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .doTry()
+        .process(new ProcessorFail())
+        .to("mock:result")
+    .doCatch(IOException.class, IllegalStateException.class)
+        .onWhen(exceptionMessage().contains("Damn"))
+        .to("mock:catch")
+    .doCatch(CamelExchangeException.class)
+        .to("mock:catchCamel")
+    .doFinally()
+        .to("mock:finally")
+    .end();
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:start"/>
+  <doTry>
+    <process ref="processorFail"/>
+    <to uri="mock:result"/>
+    <doCatch>
+      <exception>java.io.IOException</exception>
+      <exception>java.lang.IllegalStateException</exception>
+      <onWhen>
+        <simple>${exception.message} contains 'Damn'</simple>
+      </onWhen>
+      <to uri="mock:catch"/>
+    </doCatch>
+    <doCatch>
+      <exception>org.apache.camel.CamelExchangeException</exception>
+      <to uri="mock:catchCamel"/>
+    </doCatch>
+    <doFinally>
+       <to uri="mock:finally"/>
+    </doFinally>
+  </doTry>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:start
+      steps:
+        - doTry:
+            steps:
+              - process:
+                  ref: processorFail
+              - to:
+                  uri: mock:result
+              - doCatch:
+                  exception:
+                    - java.io.IOException
+                    - java.lang.IllegalStateException
+                  steps:
+                    - onWhen:
+                        expression:
+                          simple:
+                            expression: "${exception.message} contains 'Damn'"
+                    - to:
+                        uri: mock:catch
+              - doCatch:
+                  exception:
+                    - org.apache.camel.CamelExchangeException
+                  steps:
+                    - to:
+                        uri: mock:catchCamel
+              - doFinally:
+                  steps:
+                    - to:
+                        uri: mock:finally
+----
+====
+
+=== Use end() to end the block
+
+When using Java DSL you must use `end()` to indicate where the try ... catch 
... finally block ends.
+If the route has a `doFinally`, then the `end()` should be at the end of the 
finally block.
+If there is no `doFinally`, then the `end()` should be at the end of the last 
`doCatch`.
+
+TIP: Instead of `end()` you can use `endDoTry()` to end and _return back_ to 
the try ... catch scope.
+
+=== Nested doTry ... doCatch
+
+When nesting `doTry ... doCatch` from an outer `doTry ... doCatch` EIP,
+pay extra attention when using Java DSL as the Java programming language is 
not _indent aware_
+so you may write code that is indented in a way where you think a catch block 
is associated with the other doTry, but it is not.
+
+Given the following Java DSL:
+
+[source,java]
+----
+from("direct:test")
+    .doTry().
+        doTry().
+            throwException(new IllegalArgumentException("Forced by me"))
+        .doCatch(Exception.class)
+            .log("docatch 1")
+            .throwException(new IllegalArgumentException("Second forced by 
me"))
+    .doCatch(Exception.class)
+        .log("docatch 2")
+    .end();
+----
+
+Both `docatch 1` and `docatch 2` are in the inner `doTry`, and the outer 
`doTry` has no catch blocks.
+Use `endDoTry()` to end the blocks correctly:
+
+[source,java]
+----
+from("direct:test")
+    .doTry().
+        doTry().
+            throwException(new IllegalArgumentException("Forced by me"))
+        .doCatch(Exception.class)
+            .log("docatch 1")
+            .throwException(new IllegalArgumentException("Second forced by 
me"))
+         .endDoTry() // end this doCatch block
+     .endDoTry() // end the inner doTry
+    .doCatch(Exception.class)
+        .log("docatch 2")
+    .end();
+----
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pausable-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pausable-eip.adoc
new file mode 100644
index 000000000000..20bc377177a8
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pausable-eip.adoc
@@ -0,0 +1,54 @@
+= Pausable EIP
+:doctitle: Pausable
+:shortname: pausable
+:description: Pauses a consumer based on a condition, allowing it to be 
resumed later from the last known offset. Useful for controlling ingestion rate 
on polling consumers.
+:since: 
+:supportlevel: Stable
+:tabs-sync-option:
+
+The Pausable EIP provides pause and resume features for supported consumers.
+With this EIP, it is possible to implement logic that controls the behavior of 
the consumer
+based on conditions that are external to the component. For instance, it makes 
it possible
+to pause the consumer if an external system becomes unavailable.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Example
+
+To use the Pausable EIP, you need an instance of a consumer listener along 
with a predicate
+that tests whether consumption should continue.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("kafka:topic")
+    .pausable(new KafkaConsumerListener(), o -> canContinue())
+    .process(exchange -> LOG.info("Received: {}", 
exchange.getMessage().getBody()))
+    .to("direct:destination");
+----
+====
+
+You can also integrate the Pausable EIP with the 
xref:circuitBreaker-eip.adoc[Circuit Breaker EIP]
+to pause consumption based on downstream system availability:
+
+[source,java]
+----
+from("kafka:topic")
+    .pausable(new KafkaConsumerListener(), o -> canContinue())
+    .circuitBreaker()
+        .resilience4jConfiguration().circuitBreaker("pausableCircuit").end()
+        .to("direct:downstream")
+    .end();
+----
+
+== See Also
+
+* xref:resume-strategies.adoc[Resume Strategies]
+* xref:resumable-eip.adoc[Resumable EIP]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc
new file mode 100644
index 000000000000..702e92e4970a
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc
@@ -0,0 +1,56 @@
+= Resumable EIP
+:doctitle: Resumable
+:shortname: resumable
+:description: Enables resuming processing from the last known offset after a 
restart, using a resume strategy to track and restore position
+:since: 
+:supportlevel: Stable
+:tabs-sync-option:
+
+The Resumable EIP allows consuming data from the last known offset after a 
restart,
+using a resume strategy to track and restore the position.
+
+This is useful when consuming large data sources where you want to avoid 
reprocessing
+data that has already been consumed. The resume strategy points the consumer 
to the exact
+offset to resume operations.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Example
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+KafkaResumeStrategyConfigurationBuilder kafkaConfigurationBuilder =
+    KafkaResumeStrategyConfigurationBuilder.newBuilder()
+        .withBootstrapServers("kafka-address:9092")
+        .withTopic("offset")
+        .withResumeCache(new MyChoiceOfResumeCache<>(100));
+
+from("some:component")
+    .resumable().configuration(kafkaConfigurationBuilder)
+    .process(this::process);
+----
+====
+
+=== Intermittent Mode
+
+Enable intermittent mode to avoid updating the offset for every exchange:
+
+[source,java]
+----
+from("some:component")
+    .resumable(new MyTestResumeStrategy()).intermittent(true)
+    .process(this::process);
+----
+
+== See Also
+
+* xref:resume-strategies.adoc[Resume Strategies]
+* xref:pausable-eip.adoc[Pausable EIP]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenizer-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenizer-eip.adoc
new file mode 100644
index 000000000000..db34196f5afa
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenizer-eip.adoc
@@ -0,0 +1,109 @@
+= Specialized tokenizer for AI applications EIP
+:doctitle: Specialized tokenizer for AI applications
+:shortname: tokenizer
+:description: Tokenizes the message body for AI processing, splitting text 
into chunks suitable for embedding or LLM input
+:since: 4.8
+:supportlevel: Stable
+:tabs-sync-option:
+
+The Tokenizer EIP provides support to tokenize (chunk) larger blocks of text 
into text segments
+that can be used when interacting with LLMs. Tokenization is particularly 
helpful when used with
+https://en.wikipedia.org/wiki/Vector_database[vector databases] to provide 
better and more contextual search results
+for 
https://en.wikipedia.org/wiki/Retrieval-augmented_generation[retrieval-augmented
 generation (RAG)].
+
+This EIP uses the 
https://docs.langchain4j.dev/tutorials/rag/#document-splitter[LangChain4j 
document splitter]
+to handle chunking.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Supported Splitters
+
+The following type of splitters is supported:
+
+* By paragraph: using the DSL `tokenizer().byParagraph()`
+* By sentence: using the DSL `tokenizer().bySentence()`
+* By word: using the DSL `tokenizer().byWord()`
+* By line: using the DSL `tokenizer().byLine()`
+* By character: using the DSL `tokenizer().byCharacter()`
+
+== Supported Tokenizers
+
+The following tokenizers are supported:
+
+* OpenAI: using `LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI`
+* Azure: using `LangChain4jTokenizerDefinition.TokenizerType.AZURE`
+* Qwen: using `LangChain4jTokenizerDefinition.TokenizerType.QWEN`
+
+== Example
+
+The tokenization creates a composite message (an array of Strings). This 
composite message can be split
+using the xref:split-eip.adoc[Split EIP] so that each text segment is sent 
separately to an endpoint.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .tokenize(tokenizer()
+            .byParagraph()
+                .maxSegmentSize(1024)
+                .maxOverlap(10)
+                .using(LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI)
+                .end())
+    .split().body()
+    .to("mock:result");
+----
+====
+
+=== Tokenize by tokens
+
+Starting with Camel 4.12, the code defaults to using segment sizes for 
determining the maximum amount of data to tokenize.
+To tokenize by tokens, you must specify the underlying model:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .tokenize(tokenizer()
+            .byParagraph()
+                .maxTokens(1024, "gpt-4o-mini")
+                .maxOverlap(10)
+                .using(LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI)
+                .end())
+    .split().body()
+    .to("mock:result");
+----
+====
+
+== Dependencies
+
+Maven users will need to add the following dependency:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel</groupId>
+    <artifactId>camel-langchain4j-tokenizer</artifactId>
+    <version>x.x.x</version>
+    <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+The application must also provide the specific tokenizer implementation from 
LangChain4j:
+
+* OpenAI: `dev.langchain4j:langchain4j-open-ai`
+* Azure: `dev.langchain4j:langchain4j-azure-open-ai`
+* Qwen: `dev.langchain4j:langchain4j-dashscope`
+
+== See Also
+
+* xref:split-eip.adoc[Split EIP]
diff --git a/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc
index 69a90e9d895a..1b595a329704 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc
@@ -3,6 +3,7 @@
 
 * xref:eips:enterprise-integration-patterns.adoc[Enterprise Integration 
Patterns]
 * xref:eips:ai-patterns.adoc[AI Patterns]
+** xref:a2aSubTask-eip.adoc[A2A Sub Task]
 ** xref:aggregate-eip.adoc[Aggregate]
 ** xref:batchConfig-eip.adoc[BatchConfig]
 ** xref:bean-eip.adoc[Bean]
@@ -22,6 +23,7 @@
 ** xref:customLoadBalancer-eip.adoc[Custom Load Balancer]
 ** xref:dead-letter-channel.adoc[Dead Letter Channel]
 ** xref:delay-eip.adoc[Delay]
+** xref:doTry-eip.adoc[Do Try]
 ** xref:durable-subscriber.adoc[Durable Subscriber]
 ** xref:dynamicRouter-eip.adoc[Dynamic Router]
 ** xref:enrich-eip.adoc[Enrich]
@@ -56,6 +58,7 @@
 ** xref:multicast-eip.adoc[Multicast]
 ** xref:normalizer.adoc[Normalizer]
 ** xref:onFallback-eip.adoc[On Fallback]
+** xref:pausable-eip.adoc[Pausable]
 ** xref:pipeline-eip.adoc[Pipeline]
 ** xref:point-to-point-channel.adoc[Point to Point Channel]
 ** xref:poll-eip.adoc[Poll]
@@ -75,6 +78,7 @@
 ** xref:resequence-eip.adoc[Resequence]
 ** xref:resilience4jConfiguration-eip.adoc[Resilience4j Configuration]
 ** xref:resilience4j-eip.adoc[Resilience4j EIP]
+** xref:resumable-eip.adoc[Resumable]
 ** xref:resume-strategies.adoc[Resume Strategies]
 ** xref:return-address.adoc[Return Address]
 ** xref:rollback-eip.adoc[Rollback]
@@ -93,6 +97,7 @@
 ** xref:setVariable-eip.adoc[Set Variable]
 ** xref:setVariables-eip.adoc[Set Variables]
 ** xref:sort-eip.adoc[Sort]
+** xref:tokenizer-eip.adoc[Specialized tokenizer for AI applications]
 ** xref:split-eip.adoc[Split]
 ** xref:step-eip.adoc[Step]
 ** xref:stickyLoadBalancer-eip.adoc[Sticky Load Balancer]
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/a2aSubTask-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/a2aSubTask-eip.adoc
new file mode 100644
index 000000000000..00e9a1eeb39c
--- /dev/null
+++ 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/a2aSubTask-eip.adoc
@@ -0,0 +1,100 @@
+= A2A Sub Task EIP
+:doctitle: A2A Sub Task
+:shortname: a2aSubTask
+:description: Groups route steps and emits A2A (Agent-to-Agent) protocol 
progress events before, after, or when the grouped work fails
+:since: 4.21
+:supportlevel: Stable
+:tabs-sync-option:
+
+The A2A Sub Task EIP groups route steps and emits 
https://a2a-protocol.org/latest/specification/[A2A protocol] progress events
+before, after, or when the grouped work fails.
+
+This is useful for xref:ROOT:a2a-component.adoc[A2A] consumer routes where you 
want to report scoped progress updates
+to the calling agent as the route processes different stages of work.
+
+The `emitBefore`, `emitAfter`, and `emitOnError` fields are optional and are 
evaluated as
+xref:languages:simple-language.adoc[Simple] expressions against the current 
Exchange.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Example
+
+In this example, the route groups two stages of work and emits progress events 
for each:
+
+[tabs]
+====
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: a2a:classpath:agent-card.json
+      steps:
+        - a2aSubTask:
+            id: search-docs-progress
+            emitBefore: "Searching docs..."
+            emitAfter: "Docs found: ${body.size()}"
+            emitOnError: "Error searching docs: ${exception.message}"
+            steps:
+              - to:
+                  uri: "elasticsearch:docs?operation=Search"
+        - a2aSubTask:
+            id: draft-answer-progress
+            emitBefore: "Drafting answer..."
+            emitAfter: "Answer drafted: ${body}"
+            emitOnError: "Error drafting answer: ${exception.message}"
+            steps:
+              - bean:
+                  ref: answerDraftingService
+                  method: draft
+        - setBody:
+            simple: "Final answer: ${body}"
+----
+
+Java::
++
+[source,java]
+----
+from("a2a:classpath:agent-card.json")
+    .a2aSubTask()
+        .id("search-docs-progress")
+        .emitBefore("Searching docs...")
+        .emitAfter("Docs found: ${body.size()}")
+        .emitOnError("Error searching docs: ${exception.message}")
+        .to("elasticsearch:docs?operation=Search")
+    .end()
+    .a2aSubTask()
+        .id("draft-answer-progress")
+        .emitBefore("Drafting answer...")
+        .emitAfter("Answer drafted: ${body}")
+        .emitOnError("Error drafting answer: ${exception.message}")
+        .bean(answerDraftingService, "draft")
+    .end()
+    .setBody(simple("Final answer: ${body}"));
+----
+====
+
+== Behavior
+
+* The nested `steps` behave like normal Camel route steps.
+* If the nested steps fail, `emitOnError` can access the original exception 
via `${exception.message}`,
+  and the original exception continues to propagate through the route.
+* Failures while evaluating the `emitBefore`, `emitAfter`, or `emitOnError` 
expressions are route failures.
+* Failures while storing or notifying the resulting A2A progress event are 
best-effort: they are logged
+  at debug level and do not stop nested work, fail an otherwise successful 
exchange, or replace
+  the original nested-step exception.
+* By default, emitting progress outside an active A2A task context is a no-op.
+  Set `failIfNoTaskContext=true` when a route should fail if the current 
Exchange
+  does not carry an active A2A task.
+* Use stable `id` values on long-lived sub-tasks so route tracing and 
route-structure views keep stable node names.
+
+== See Also
+
+* xref:ROOT:a2a-component.adoc[A2A Component]
+* xref:others:a2a-consumer.adoc[A2A Consumer Guide]
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/doTry-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/doTry-eip.adoc
new file mode 100644
index 000000000000..0b43a808862f
--- /dev/null
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/doTry-eip.adoc
@@ -0,0 +1,246 @@
+= Do Try EIP
+:doctitle: Do Try
+:shortname: doTry
+:description: Wraps route steps in a try-catch-finally block for fine-grained 
exception handling within the route
+:since: 
+:supportlevel: Stable
+:tabs-sync-option:
+
+Camel supports the Java equivalent of try, catch, and finally directly in the 
DSL.
+It aims to work like its Java sisters but with more power.
+
+In Camel, we prefix the keywords with `do` to avoid having same keyword as 
Java.
+So we have:
+
+- `doTry`
+- `doCatch`
+- `doFinally`
+- `end` to end the block in Java DSL
+
+[IMPORTANT]
+====
+When using `doTry ... doCatch ... doFinally`, the regular Camel
+xref:manual::error-handler.adoc[Error Handler] is not in use; meaning any
+`onException` or the likes does not trigger.
+
+The reason is that
+`doTry ... doCatch ... doFinally` is in fact its own error handler and mimics 
how try/catch/finally works in Java.
+====
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Using doTry ... doCatch ... doFinally
+
+In the route below we have all of them in action:
+
+[tabs]
+====
+
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .doTry()
+        .process(new ProcessorFail())
+        .to("mock:result")
+    .doCatch(IOException.class, IllegalStateException.class)
+        .to("mock:catch")
+    .doFinally()
+        .to("mock:finally")
+    .end();
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:start"/>
+  <doTry>
+    <process ref="processorFail"/>
+    <to uri="mock:result"/>
+    <doCatch>
+      <exception>java.io.IOException</exception>
+      <exception>java.lang.IllegalStateException</exception>
+      <to uri="mock:catch"/>
+    </doCatch>
+    <doFinally>
+       <to uri="mock:finally"/>
+    </doFinally>
+  </doTry>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:start
+      steps:
+        - doTry:
+            steps:
+              - process:
+                  ref: processorFail
+              - to:
+                  uri: mock:result
+              - doCatch:
+                  exception:
+                    - java.io.IOException
+                    - java.lang.IllegalStateException
+                  steps:
+                    - to:
+                        uri: mock:catch
+              - doFinally:
+                  steps:
+                    - to:
+                        uri: mock:finally
+----
+====
+
+=== Using onWhen with doCatch
+
+You can use xref:manual::predicate.adoc[Predicates] with `doCatch` to make it 
determine at runtime if the block should be triggered or not.
+In this example, we only want to trigger if the caused exception message 
contains the word *Damn*.
+
+[tabs]
+====
+
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .doTry()
+        .process(new ProcessorFail())
+        .to("mock:result")
+    .doCatch(IOException.class, IllegalStateException.class)
+        .onWhen(exceptionMessage().contains("Damn"))
+        .to("mock:catch")
+    .doCatch(CamelExchangeException.class)
+        .to("mock:catchCamel")
+    .doFinally()
+        .to("mock:finally")
+    .end();
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:start"/>
+  <doTry>
+    <process ref="processorFail"/>
+    <to uri="mock:result"/>
+    <doCatch>
+      <exception>java.io.IOException</exception>
+      <exception>java.lang.IllegalStateException</exception>
+      <onWhen>
+        <simple>${exception.message} contains 'Damn'</simple>
+      </onWhen>
+      <to uri="mock:catch"/>
+    </doCatch>
+    <doCatch>
+      <exception>org.apache.camel.CamelExchangeException</exception>
+      <to uri="mock:catchCamel"/>
+    </doCatch>
+    <doFinally>
+       <to uri="mock:finally"/>
+    </doFinally>
+  </doTry>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:start
+      steps:
+        - doTry:
+            steps:
+              - process:
+                  ref: processorFail
+              - to:
+                  uri: mock:result
+              - doCatch:
+                  exception:
+                    - java.io.IOException
+                    - java.lang.IllegalStateException
+                  steps:
+                    - onWhen:
+                        expression:
+                          simple:
+                            expression: "${exception.message} contains 'Damn'"
+                    - to:
+                        uri: mock:catch
+              - doCatch:
+                  exception:
+                    - org.apache.camel.CamelExchangeException
+                  steps:
+                    - to:
+                        uri: mock:catchCamel
+              - doFinally:
+                  steps:
+                    - to:
+                        uri: mock:finally
+----
+====
+
+=== Use end() to end the block
+
+When using Java DSL you must use `end()` to indicate where the try ... catch 
... finally block ends.
+If the route has a `doFinally`, then the `end()` should be at the end of the 
finally block.
+If there is no `doFinally`, then the `end()` should be at the end of the last 
`doCatch`.
+
+TIP: Instead of `end()` you can use `endDoTry()` to end and _return back_ to 
the try ... catch scope.
+
+=== Nested doTry ... doCatch
+
+When nesting `doTry ... doCatch` from an outer `doTry ... doCatch` EIP,
+pay extra attention when using Java DSL as the Java programming language is 
not _indent aware_
+so you may write code that is indented in a way where you think a catch block 
is associated with the other doTry, but it is not.
+
+Given the following Java DSL:
+
+[source,java]
+----
+from("direct:test")
+    .doTry().
+        doTry().
+            throwException(new IllegalArgumentException("Forced by me"))
+        .doCatch(Exception.class)
+            .log("docatch 1")
+            .throwException(new IllegalArgumentException("Second forced by 
me"))
+    .doCatch(Exception.class)
+        .log("docatch 2")
+    .end();
+----
+
+Both `docatch 1` and `docatch 2` are in the inner `doTry`, and the outer 
`doTry` has no catch blocks.
+Use `endDoTry()` to end the blocks correctly:
+
+[source,java]
+----
+from("direct:test")
+    .doTry().
+        doTry().
+            throwException(new IllegalArgumentException("Forced by me"))
+        .doCatch(Exception.class)
+            .log("docatch 1")
+            .throwException(new IllegalArgumentException("Second forced by 
me"))
+         .endDoTry() // end this doCatch block
+     .endDoTry() // end the inner doTry
+    .doCatch(Exception.class)
+        .log("docatch 2")
+    .end();
+----
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/pausable-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/pausable-eip.adoc
new file mode 100644
index 000000000000..20bc377177a8
--- /dev/null
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/pausable-eip.adoc
@@ -0,0 +1,54 @@
+= Pausable EIP
+:doctitle: Pausable
+:shortname: pausable
+:description: Pauses a consumer based on a condition, allowing it to be 
resumed later from the last known offset. Useful for controlling ingestion rate 
on polling consumers.
+:since: 
+:supportlevel: Stable
+:tabs-sync-option:
+
+The Pausable EIP provides pause and resume features for supported consumers.
+With this EIP, it is possible to implement logic that controls the behavior of 
the consumer
+based on conditions that are external to the component. For instance, it makes 
it possible
+to pause the consumer if an external system becomes unavailable.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Example
+
+To use the Pausable EIP, you need an instance of a consumer listener along 
with a predicate
+that tests whether consumption should continue.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("kafka:topic")
+    .pausable(new KafkaConsumerListener(), o -> canContinue())
+    .process(exchange -> LOG.info("Received: {}", 
exchange.getMessage().getBody()))
+    .to("direct:destination");
+----
+====
+
+You can also integrate the Pausable EIP with the 
xref:circuitBreaker-eip.adoc[Circuit Breaker EIP]
+to pause consumption based on downstream system availability:
+
+[source,java]
+----
+from("kafka:topic")
+    .pausable(new KafkaConsumerListener(), o -> canContinue())
+    .circuitBreaker()
+        .resilience4jConfiguration().circuitBreaker("pausableCircuit").end()
+        .to("direct:downstream")
+    .end();
+----
+
+== See Also
+
+* xref:resume-strategies.adoc[Resume Strategies]
+* xref:resumable-eip.adoc[Resumable EIP]
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc
new file mode 100644
index 000000000000..702e92e4970a
--- /dev/null
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc
@@ -0,0 +1,56 @@
+= Resumable EIP
+:doctitle: Resumable
+:shortname: resumable
+:description: Enables resuming processing from the last known offset after a 
restart, using a resume strategy to track and restore position
+:since: 
+:supportlevel: Stable
+:tabs-sync-option:
+
+The Resumable EIP allows consuming data from the last known offset after a 
restart,
+using a resume strategy to track and restore the position.
+
+This is useful when consuming large data sources where you want to avoid 
reprocessing
+data that has already been consumed. The resume strategy points the consumer 
to the exact
+offset to resume operations.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Example
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+KafkaResumeStrategyConfigurationBuilder kafkaConfigurationBuilder =
+    KafkaResumeStrategyConfigurationBuilder.newBuilder()
+        .withBootstrapServers("kafka-address:9092")
+        .withTopic("offset")
+        .withResumeCache(new MyChoiceOfResumeCache<>(100));
+
+from("some:component")
+    .resumable().configuration(kafkaConfigurationBuilder)
+    .process(this::process);
+----
+====
+
+=== Intermittent Mode
+
+Enable intermittent mode to avoid updating the offset for every exchange:
+
+[source,java]
+----
+from("some:component")
+    .resumable(new MyTestResumeStrategy()).intermittent(true)
+    .process(this::process);
+----
+
+== See Also
+
+* xref:resume-strategies.adoc[Resume Strategies]
+* xref:pausable-eip.adoc[Pausable EIP]
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/tokenizer-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/tokenizer-eip.adoc
new file mode 100644
index 000000000000..db34196f5afa
--- /dev/null
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/tokenizer-eip.adoc
@@ -0,0 +1,109 @@
+= Specialized tokenizer for AI applications EIP
+:doctitle: Specialized tokenizer for AI applications
+:shortname: tokenizer
+:description: Tokenizes the message body for AI processing, splitting text 
into chunks suitable for embedding or LLM input
+:since: 4.8
+:supportlevel: Stable
+:tabs-sync-option:
+
+The Tokenizer EIP provides support to tokenize (chunk) larger blocks of text 
into text segments
+that can be used when interacting with LLMs. Tokenization is particularly 
helpful when used with
+https://en.wikipedia.org/wiki/Vector_database[vector databases] to provide 
better and more contextual search results
+for 
https://en.wikipedia.org/wiki/Retrieval-augmented_generation[retrieval-augmented
 generation (RAG)].
+
+This EIP uses the 
https://docs.langchain4j.dev/tutorials/rag/#document-splitter[LangChain4j 
document splitter]
+to handle chunking.
+
+== EIP options
+
+// eip options: START
+include::partial$eip-options.adoc[]
+// eip options: END
+
+== Supported Splitters
+
+The following type of splitters is supported:
+
+* By paragraph: using the DSL `tokenizer().byParagraph()`
+* By sentence: using the DSL `tokenizer().bySentence()`
+* By word: using the DSL `tokenizer().byWord()`
+* By line: using the DSL `tokenizer().byLine()`
+* By character: using the DSL `tokenizer().byCharacter()`
+
+== Supported Tokenizers
+
+The following tokenizers are supported:
+
+* OpenAI: using `LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI`
+* Azure: using `LangChain4jTokenizerDefinition.TokenizerType.AZURE`
+* Qwen: using `LangChain4jTokenizerDefinition.TokenizerType.QWEN`
+
+== Example
+
+The tokenization creates a composite message (an array of Strings). This 
composite message can be split
+using the xref:split-eip.adoc[Split EIP] so that each text segment is sent 
separately to an endpoint.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .tokenize(tokenizer()
+            .byParagraph()
+                .maxSegmentSize(1024)
+                .maxOverlap(10)
+                .using(LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI)
+                .end())
+    .split().body()
+    .to("mock:result");
+----
+====
+
+=== Tokenize by tokens
+
+Starting with Camel 4.12, the code defaults to using segment sizes for 
determining the maximum amount of data to tokenize.
+To tokenize by tokens, you must specify the underlying model:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:start")
+    .tokenize(tokenizer()
+            .byParagraph()
+                .maxTokens(1024, "gpt-4o-mini")
+                .maxOverlap(10)
+                .using(LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI)
+                .end())
+    .split().body()
+    .to("mock:result");
+----
+====
+
+== Dependencies
+
+Maven users will need to add the following dependency:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel</groupId>
+    <artifactId>camel-langchain4j-tokenizer</artifactId>
+    <version>x.x.x</version>
+    <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+The application must also provide the specific tokenizer implementation from 
LangChain4j:
+
+* OpenAI: `dev.langchain4j:langchain4j-open-ai`
+* Azure: `dev.langchain4j:langchain4j-azure-open-ai`
+* Qwen: `dev.langchain4j:langchain4j-dashscope`
+
+== See Also
+
+* xref:split-eip.adoc[Split EIP]
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index b8886fe3b73e..9ac743df5eac 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -752,9 +752,9 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("route-structure");
         if (dc != null) {
-            String filter = root.getString("filter");
-            String brief = root.getString("brief");
-            String metric = root.getString("metric");
+            String filter = root.getStringOrDefault("filter", "*");
+            String brief = root.getStringOrDefault("brief", "false");
+            String metric = root.getStringOrDefault("metric", "false");
             JsonObject json
                     = (JsonObject) dc.call(DevConsole.MediaType.JSON,
                             Map.of("filter", filter, "brief", brief, "metric", 
metric));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index e29e3f22e96c..06005dc5b830 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -115,6 +115,7 @@ class ActionsPopup {
     private final ThemePopup themePopup = new ThemePopup();
     private final SettingsPopup settingsPopup = new SettingsPopup();
     private final DocViewerPopup docViewerPopup = new DocViewerPopup();
+    private final OptionsViewerPopup optionsViewerPopup = new 
OptionsViewerPopup();
 
     private final ExampleBrowserPopup exampleBrowserPopup;
     private final RunOptionsForm runOptionsForm = new RunOptionsForm();
@@ -353,6 +354,7 @@ class ActionsPopup {
                 || folderInputPopup.isVisible()
                 || runOptionsForm.isVisible()
                 || docViewerPopup.isVisible()
+                || optionsViewerPopup.isVisible()
                 || infraBrowserPopup.isVisible()
                 || mcpLogPopup.isVisible() || aiLogPopup.isVisible() || 
doctorPopup.isVisible()
                 || sendMessagePopup.isVisible() || stopAllPopup.isVisible() || 
captionOverlay.isInlineMode();
@@ -500,11 +502,22 @@ class ActionsPopup {
         if (docViewerPopup.isVisible()) {
             boolean wasPicker = docViewerPopup.isPickerVisible();
             docViewerPopup.handleKeyEvent(ke);
-            if (wasPicker && !docViewerPopup.isVisible() && ke.isCancel()) {
+            if (!docViewerPopup.isVisible() && 
docViewerPopup.consumeWantsOptions()) {
+                openOptions(docViewerPopup.getCatalogEntryName(), 
docViewerPopup.getCatalogEntryKind(),
+                        docViewerPopup.getCatalogEntryRef());
+            } else if (wasPicker && !docViewerPopup.isVisible() && 
ke.isCancel()) {
                 showActionsMenu = true;
             }
             return true;
         }
+        if (optionsViewerPopup.isVisible()) {
+            optionsViewerPopup.handleKeyEvent(ke);
+            if (!optionsViewerPopup.isVisible() && 
optionsViewerPopup.consumeWantsDoc()) {
+                openDocForEntry(optionsViewerPopup.getCurrentName(), 
optionsViewerPopup.getCurrentKind(),
+                        optionsViewerPopup.getCurrentCatalog());
+            }
+            return true;
+        }
         if (infraBrowserPopup.isVisible()) {
             infraBrowserPopup.handleKeyEvent(ke);
             if (!infraBrowserPopup.isVisible() && ke.isCancel()) {
@@ -763,6 +776,9 @@ class ActionsPopup {
         if (docViewerPopup.isVisible()) {
             return docViewerPopup.handleMouseEvent(me);
         }
+        if (optionsViewerPopup.isVisible()) {
+            return optionsViewerPopup.handleMouseEvent(me);
+        }
         if (folderInputPopup.isVisible()) {
             return folderInputPopup.handleMouseEvent(me);
         }
@@ -834,6 +850,9 @@ class ActionsPopup {
         if (docViewerPopup.isVisible()) {
             docViewerPopup.render(frame, area);
         }
+        if (optionsViewerPopup.isVisible()) {
+            optionsViewerPopup.render(frame, area);
+        }
         if (mcpLogPopup.isVisible()) {
             mcpLogPopup.render(frame, area);
         }
@@ -883,6 +902,10 @@ class ActionsPopup {
             docViewerPopup.renderFooter(spans);
             return;
         }
+        if (optionsViewerPopup.isVisible()) {
+            optionsViewerPopup.renderFooter(spans);
+            return;
+        }
         if (runOptionsForm.isVisible()) {
             runOptionsForm.renderFooter(spans);
             return;
@@ -1090,6 +1113,39 @@ class ActionsPopup {
         }
     }
 
+    void openMarkdown(String title, String markdown) {
+        showActionsMenu = false;
+        docViewerPopup.openMarkdown(title, markdown);
+    }
+
+    void openOptions(String name, String kind, 
org.apache.camel.catalog.CamelCatalog catalog) {
+        showActionsMenu = false;
+        optionsViewerPopup.open(name, kind, catalog);
+    }
+
+    void openCatalogDoc(String name, String kind, 
org.apache.camel.catalog.CamelCatalog catalog) {
+        showActionsMenu = false;
+        openDocForEntry(name, kind, catalog);
+    }
+
+    private void openDocForEntry(String name, String kind, 
org.apache.camel.catalog.CamelCatalog catalog) {
+        if (catalog == null) {
+            return;
+        }
+        String docName = switch (kind) {
+            case "component" -> name + "-component";
+            case "dataformat" -> name + "-dataformat";
+            case "language" -> name + "-language";
+            case "eip" -> name + "-eip";
+            default -> name;
+        };
+        String adoc = catalog.asciiDoc(docName);
+        if (adoc != null) {
+            String md = DocHelper.asciidocToMarkdown(adoc);
+            docViewerPopup.openCatalogDoc(name, md, name, kind, catalog);
+        }
+    }
+
     private void setNotification(String msg, boolean error) {
         if (notificationCallback != null) {
             notificationCallback.accept(msg, error);
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 c4807f6c9891..7d8ee4726e57 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
@@ -268,6 +268,9 @@ public class CamelMonitor extends CamelCommand {
         actionsPopup.setMonitorContext(ctx);
         actionsPopup.setNotificationCallback((msg, error) -> 
setNotification(msg, error));
         ctx.notificationCallback = (msg, error) -> setNotification(msg, error);
+        ctx.openMarkdownCallback = actionsPopup::openMarkdown;
+        ctx.openOptionsCallback = actionsPopup::openOptions;
+        ctx.openCatalogDocCallback = actionsPopup::openCatalogDoc;
         actionsPopup.setResetStatsAction(this::resetStats);
         shellPanel.setContext(ctx);
         aiPanel.setContext(ctx);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
index 825790433d5e..6c34acfb0ed6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -43,7 +44,9 @@ import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.tooling.model.ArtifactModel;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -55,7 +58,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
  */
 class CatalogTab extends AbstractTableTab {
 
-    private static final String[] SCOPES = { "all", "component", "dataformat", 
"language", "other" };
+    private static final String[] SCOPES = { "all", "component", "dataformat", 
"language", "other", "eip" };
 
     private final AtomicBoolean loading = new AtomicBoolean(false);
 
@@ -64,6 +67,7 @@ class CatalogTab extends AbstractTableTab {
     private String filterTerm;
     private int scopeIndex;
     private boolean fullCatalog;
+    private CamelCatalog catalog;
     private List<CatalogEntry> allEntries = Collections.emptyList();
     private List<CatalogEntry> filteredEntries = Collections.emptyList();
     private String lastPid;
@@ -94,6 +98,7 @@ class CatalogTab extends AbstractTableTab {
         filterTerm = null;
         filterInputActive = false;
         scopeIndex = 0;
+        catalog = null;
         lastPid = null;
         errorMessage = null;
         dataLoaded = false;
@@ -134,6 +139,14 @@ class CatalogTab extends AbstractTableTab {
             loadCatalogData();
             return true;
         }
+        if (ke.isCharIgnoreCase('d')) {
+            openDocViewer();
+            return true;
+        }
+        if (ke.isCharIgnoreCase('o')) {
+            openOptionsViewer();
+            return true;
+        }
         return false;
     }
 
@@ -297,11 +310,13 @@ class CatalogTab extends AbstractTableTab {
         addDetailField(lines, "Title", entry.title, area.width());
         addDetailField(lines, "Description", entry.description, area.width());
         addDetailField(lines, "Kind", entry.kind, area.width());
-        String maven = entry.groupId + ":" + entry.artifactId;
-        if (entry.version != null) {
-            maven += ":" + entry.version;
+        if (entry.groupId != null) {
+            String maven = entry.groupId + ":" + entry.artifactId;
+            if (entry.version != null) {
+                maven += ":" + entry.version;
+            }
+            addDetailField(lines, "Maven", maven, area.width());
         }
-        addDetailField(lines, "Maven", maven, area.width());
         if (entry.firstVersion != null) {
             addDetailField(lines, "Since", entry.firstVersion, area.width());
         }
@@ -334,6 +349,34 @@ class CatalogTab extends AbstractTableTab {
                 area);
     }
 
+    private void openDocViewer() {
+        if (ctx.openCatalogDocCallback == null || catalog == null) {
+            return;
+        }
+        List<CatalogEntry> sorted = new ArrayList<>(filteredEntries);
+        sorted.sort(this::sortEntry);
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= sorted.size()) {
+            return;
+        }
+        CatalogEntry entry = sorted.get(sel);
+        ctx.openCatalogDocCallback.accept(entry.name, entry.kind, catalog);
+    }
+
+    private void openOptionsViewer() {
+        if (ctx.openOptionsCallback == null || catalog == null) {
+            return;
+        }
+        List<CatalogEntry> sorted = new ArrayList<>(filteredEntries);
+        sorted.sort(this::sortEntry);
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= sorted.size()) {
+            return;
+        }
+        CatalogEntry entry = sorted.get(sel);
+        ctx.openOptionsCallback.accept(entry.name, entry.kind, catalog);
+    }
+
     private void addDetailField(List<Line> lines, String label, String value, 
int width) {
         if (value == null || value.isEmpty()) {
             return;
@@ -377,6 +420,8 @@ class CatalogTab extends AbstractTableTab {
         hint(spans, "s", "sort");
         hint(spans, "a", fullCatalog ? "app only" : "all");
         hint(spans, "f", "scope [" + SCOPES[scopeIndex] + "]");
+        hint(spans, "d", "doc");
+        hint(spans, "o", "options");
         if (filterTerm != null) {
             spans.add(Span.styled("  /", Theme.label().bold()));
             spans.add(Span.raw("\"" + filterTerm + "\"  "));
@@ -400,6 +445,7 @@ class CatalogTab extends AbstractTableTab {
             case "component" -> Style.EMPTY.fg(Theme.accent());
             case "dataformat" -> Theme.success();
             case "language" -> Theme.warning();
+            case "eip" -> Theme.info();
             default -> Style.EMPTY.dim();
         };
     }
@@ -422,7 +468,7 @@ class CatalogTab extends AbstractTableTab {
                 if (!full) {
                     DependencyLoader.LoadResult result = 
DependencyLoader.loadDependencies(info);
                     if (result.error() != null && result.entries().isEmpty()) {
-                        applyResult(Collections.emptyList(), result.error());
+                        applyResult(Collections.emptyList(), null, 
result.error());
                         return;
                     }
                     appArtifacts = new HashSet<>();
@@ -433,22 +479,23 @@ class CatalogTab extends AbstractTableTab {
                     }
                 }
 
-                CamelCatalog catalog = CatalogLoader.loadCatalog(null, 
info.camelVersion, true);
-                if (catalog == null) {
-                    applyResult(Collections.emptyList(), "Could not load 
catalog for version " + info.camelVersion);
+                CamelCatalog cat = CatalogLoader.loadCatalog(null, 
info.camelVersion, true);
+                if (cat == null) {
+                    applyResult(Collections.emptyList(), null, "Could not load 
catalog for version " + info.camelVersion);
                     return;
                 }
 
                 List<CatalogEntry> entries = new ArrayList<>();
-                collectArtifacts(catalog, "component", 
catalog.findComponentNames(), appArtifacts, entries);
-                collectArtifacts(catalog, "dataformat", 
catalog.findDataFormatNames(), appArtifacts, entries);
-                collectArtifacts(catalog, "language", 
catalog.findLanguageNames(), appArtifacts, entries);
-                collectArtifacts(catalog, "other", catalog.findOtherNames(), 
appArtifacts, entries);
+                collectArtifacts(cat, "component", cat.findComponentNames(), 
appArtifacts, entries);
+                collectArtifacts(cat, "dataformat", cat.findDataFormatNames(), 
appArtifacts, entries);
+                collectArtifacts(cat, "language", cat.findLanguageNames(), 
appArtifacts, entries);
+                collectArtifacts(cat, "other", cat.findOtherNames(), 
appArtifacts, entries);
+                collectEips(cat, info.pid, full, entries);
 
                 entries.sort((a, b) -> a.name.compareToIgnoreCase(b.name));
-                applyResult(entries, null);
+                applyResult(entries, cat, null);
             } catch (Exception e) {
-                applyResult(Collections.emptyList(), "Error: " + 
e.getMessage());
+                applyResult(Collections.emptyList(), null, "Error: " + 
e.getMessage());
             } finally {
                 loading.set(false);
             }
@@ -499,12 +546,85 @@ class CatalogTab extends AbstractTableTab {
         }
     }
 
-    private void applyResult(List<CatalogEntry> entries, String error) {
+    @SuppressWarnings("unchecked")
+    private void collectEips(CamelCatalog catalog, String pid, boolean full, 
List<CatalogEntry> entries) {
+        try {
+            Set<String> eipNames;
+            if (full) {
+                eipNames = new HashSet<>(catalog.findModelNames());
+            } else {
+                eipNames = new HashSet<>();
+                Path outputFile = ctx.getOutputFile(pid);
+                PathUtils.deleteFile(outputFile);
+
+                JsonObject root = new JsonObject();
+                root.put("action", "route-structure");
+                root.put("filter", "*");
+                root.put("brief", true);
+                root.put("metric", false);
+                PathUtils.writeTextSafely(root.toJson(), 
ctx.getActionFile(pid));
+
+                JsonObject jo = TuiHelper.pollJsonResponse(outputFile, 5000);
+                PathUtils.deleteFile(outputFile);
+                if (jo == null) {
+                    return;
+                }
+
+                List<JsonObject> routes = (List<JsonObject>) 
jo.getCollection("routes");
+                if (routes != null) {
+                    for (JsonObject route : routes) {
+                        List<JsonObject> code = (List<JsonObject>) 
route.getCollection("code");
+                        if (code != null) {
+                            for (JsonObject node : code) {
+                                String type = node.getString("type");
+                                if (type != null) {
+                                    eipNames.add(type);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            Set<String> skipEips = Set.of("when", "otherwise",
+                    "langChain4jCharacterTokenizer", 
"langChain4jLineTokenizer",
+                    "langChain4jParagraphTokenizer", 
"langChain4jSentenceTokenizer",
+                    "langChain4jWordTokenizer");
+            for (String name : eipNames) {
+                if (skipEips.contains(name)) {
+                    continue;
+                }
+                EipModel model = catalog.eipModel(name);
+                if (model == null) {
+                    continue;
+                }
+                if (full && (model.getLabel() == null || 
!model.getLabel().contains("eip"))) {
+                    continue;
+                }
+                CatalogEntry entry = new CatalogEntry();
+                entry.name = model.getName();
+                entry.kind = "eip";
+                entry.title = model.getTitle() != null ? model.getTitle() : 
name;
+                entry.description = model.getDescription() != null ? 
model.getDescription() : "";
+                entry.label = model.getLabel();
+                entry.firstVersion = model.getFirstVersion();
+                entry.deprecated = model.isDeprecated();
+                entry.deprecatedSince = model.getDeprecatedSince();
+                entry.deprecationNote = model.getDeprecationNote();
+                entries.add(entry);
+            }
+        } catch (Exception e) {
+            // ignore - EIP detection is best-effort
+        }
+    }
+
+    private void applyResult(List<CatalogEntry> entries, CamelCatalog cat, 
String error) {
         if (ctx.runner == null) {
             return;
         }
         ctx.runner.runOnRenderThread(() -> {
             allEntries = entries;
+            catalog = cat;
             errorMessage = error;
             dataLoaded = true;
             refilter();
@@ -516,6 +636,9 @@ class CatalogTab extends AbstractTableTab {
         String ft = filterTerm != null ? filterTerm.toLowerCase() : null;
         String scope = SCOPES[scopeIndex];
         for (CatalogEntry entry : allEntries) {
+            if ("all".equals(scope) && "eip".equals(entry.kind)) {
+                continue;
+            }
             if (!"all".equals(scope) && !scope.equals(entry.kind)) {
                 continue;
             }
@@ -607,18 +730,45 @@ class CatalogTab extends AbstractTableTab {
                 - **dataformat** — show only data formats
                 - **language** — show only expression languages
                 - **other** — show only miscellaneous artifacts
+                - **eip** — show EIPs detected from the running app's routes
 
                 ## Filter
 
                 Press `/` to open the filter input. Type a search term and 
press
                 `Enter` to filter by substring match on name, title, or label.
 
+                ## Documentation
+
+                Press `d` to open the full documentation for the selected 
artifact in
+                a scrollable viewer. The documentation is loaded from the 
Camel catalog
+                and converted from AsciiDoc to Markdown. Use 
`↑`/`↓`/`PgUp`/`PgDn` or
+                mouse scroll to navigate, and `Esc` to close.
+
+                ## Options
+
+                Press `o` to open the options viewer for the selected 
artifact. This
+                shows all configuration options in a structured format with 
types,
+                defaults, enum values, groups, and descriptions.
+
+                For **components**, the viewer has three tabs:
+                - **Component** — component-level options
+                - **Endpoint** — endpoint-level options
+                - **Headers** — message headers with constant names
+
+                For **data formats**, **languages**, and **others**, a single 
Options tab
+                is shown. For **EIPs**, Options and exchange Properties tabs 
are shown.
+
+                Press `←`/`→` or `Tab` to switch tabs. Use 
`↑`/`↓`/`PgUp`/`PgDn` or
+                mouse scroll to navigate, and `Esc` to close.
+
                 ## Keys
 
                 - `s` — cycle sort column (name, kind, description)
                 - `S` — reverse sort order
                 - `a` — toggle app only / full catalog
                 - `f` — cycle scope
+                - `d` — open documentation viewer
+                - `o` — open options viewer
                 - `/` — open filter
                 - `Esc` — clear filter or back
                 """;
@@ -643,7 +793,9 @@ class CatalogTab extends AbstractTableTab {
             if (e.label != null) {
                 row.put("label", e.label);
             }
-            row.put("artifactId", e.artifactId);
+            if (e.artifactId != null) {
+                row.put("artifactId", e.artifactId);
+            }
             if (e.firstVersion != null) {
                 row.put("since", e.firstVersion);
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
index c0db7d7a22dc..5d339be97f73 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
@@ -23,6 +23,8 @@ import java.util.List;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
 import dev.tamboui.markdown.MarkdownView;
 import dev.tamboui.style.Overflow;
@@ -44,6 +46,8 @@ import dev.tamboui.widgets.list.ListState;
 import dev.tamboui.widgets.list.ListWidget;
 import dev.tamboui.widgets.list.ScrollMode;
 import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
@@ -58,7 +62,15 @@ class DocViewerPopup {
     private String docTitle;
     private int docScroll;
     private Runnable onCloseCallback;
-
+    private String catalogEntryName;
+    private String catalogEntryKind;
+    private org.apache.camel.catalog.CamelCatalog catalogEntryRef;
+    private boolean wantsOptions;
+    private final TocPopup tocPopup = new TocPopup();
+    private int lastContentWidth;
+    private int lastTotalHeight;
+
+    private final ScrollbarState scrollbarState = new ScrollbarState();
     private final ListState pickerState = new ListState();
     private List<IntegrationInfo> pickerIntegrations;
     private Rect pickerRect;
@@ -105,10 +117,31 @@ class DocViewerPopup {
         return pickerIntegrations;
     }
 
+    boolean consumeWantsOptions() {
+        boolean v = wantsOptions;
+        wantsOptions = false;
+        return v;
+    }
+
+    String getCatalogEntryName() {
+        return catalogEntryName;
+    }
+
+    String getCatalogEntryKind() {
+        return catalogEntryKind;
+    }
+
+    org.apache.camel.catalog.CamelCatalog getCatalogEntryRef() {
+        return catalogEntryRef;
+    }
+
     void close() {
         showViewer = false;
         showPicker = false;
         onCloseCallback = null;
+        catalogEntryName = null;
+        catalogEntryKind = null;
+        catalogEntryRef = null;
     }
 
     void openMarkdown(String title, String markdown) {
@@ -116,6 +149,7 @@ class DocViewerPopup {
         docContent = markdown;
         docTitle = title;
         docScroll = 0;
+        lastTotalHeight = 0;
         showViewer = true;
         onCloseCallback = null;
     }
@@ -125,6 +159,15 @@ class DocViewerPopup {
         this.onCloseCallback = onClose;
     }
 
+    void openCatalogDoc(
+            String title, String markdown, String entryName, String entryKind,
+            org.apache.camel.catalog.CamelCatalog catalog) {
+        openMarkdown(title, markdown);
+        this.catalogEntryName = entryName;
+        this.catalogEntryKind = entryKind;
+        this.catalogEntryRef = catalog;
+    }
+
     void openLines(String title, List<Line> lines) {
         docContent = null;
         docLines = lines;
@@ -136,6 +179,14 @@ class DocViewerPopup {
 
     boolean handleKeyEvent(KeyEvent ke) {
         if (showViewer) {
+            if (tocPopup.isVisible()) {
+                tocPopup.handleKeyEvent(ke);
+                TocPopup.TocEntry entry = tocPopup.consumePendingEntry();
+                if (entry != null) {
+                    jumpToHeading(entry);
+                }
+                return true;
+            }
             if (ke.isCancel()) {
                 showViewer = false;
                 Runnable cb = onCloseCallback;
@@ -143,6 +194,14 @@ class DocViewerPopup {
                 if (cb != null) {
                     cb.run();
                 }
+            } else if (ke.isCharIgnoreCase('t') && docContent != null) {
+                List<TocPopup.TocEntry> headings = 
TocPopup.extractHeadings(docContent);
+                if (!headings.isEmpty()) {
+                    tocPopup.open(headings);
+                }
+            } else if (ke.isCharIgnoreCase('o') && catalogEntryName != null) {
+                wantsOptions = true;
+                showViewer = false;
             } else if (ke.isUp() || ke.isChar('k')) {
                 docScroll = Math.max(0, docScroll - 1);
             } else if (ke.isDown() || ke.isChar('j')) {
@@ -177,6 +236,14 @@ class DocViewerPopup {
 
     boolean handleMouseEvent(MouseEvent me) {
         if (showViewer) {
+            if (tocPopup.isVisible()) {
+                tocPopup.handleMouseEvent(me);
+                TocPopup.TocEntry entry = tocPopup.consumePendingEntry();
+                if (entry != null) {
+                    jumpToHeading(entry);
+                }
+                return true;
+            }
             if (me.kind() == MouseEventKind.SCROLL_UP) {
                 docScroll = Math.max(0, docScroll - 3);
                 return true;
@@ -190,6 +257,19 @@ class DocViewerPopup {
         return false;
     }
 
+    private void jumpToHeading(TocPopup.TocEntry entry) {
+        if (docContent == null || lastContentWidth <= 0) {
+            return;
+        }
+        String prefix = docContent.substring(0, Math.min(entry.charOffset(), 
docContent.length()));
+        int offset = MarkdownView.builder()
+                .source(prefix)
+                .styles(Theme.markdownStyles())
+                .build()
+                .computeHeight(lastContentWidth);
+        docScroll = offset;
+    }
+
     void render(Frame frame, Rect area) {
         if (showPicker) {
             renderPicker(frame, area);
@@ -201,8 +281,14 @@ class DocViewerPopup {
 
     void renderFooter(List<Span> spans) {
         if (showViewer) {
+            hint(spans, "Esc", "back");
             hint(spans, "↑↓", "scroll");
-            hintLast(spans, "Esc", "back");
+            if (docContent != null) {
+                hint(spans, "t", "toc");
+            }
+            if (catalogEntryName != null) {
+                hintLast(spans, "o", "options");
+            }
         } else if (showPicker) {
             hint(spans, "↑↓", "navigate");
             hint(spans, "Enter", "view");
@@ -399,7 +485,6 @@ class DocViewerPopup {
 
     private void renderViewer(Frame frame, Rect area) {
         frame.renderWidget(Clear.INSTANCE, area);
-        Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() - 
4, area.height() - 2);
         Title title;
         if (docTitle != null && docTitle.startsWith("Failed:")) {
             String rest = docTitle.substring("Failed:".length());
@@ -409,16 +494,35 @@ class DocViewerPopup {
         } else {
             title = Title.from(" " + docTitle + " ");
         }
+        List<Span> footerSpans = new ArrayList<>();
+        footerSpans.add(Span.styled(" Esc ", Theme.hintKey()));
+        footerSpans.add(Span.raw(" back  "));
+        footerSpans.add(Span.styled(" ↑↓ ", Theme.hintKey()));
+        footerSpans.add(Span.raw(" scroll  "));
+        if (docContent != null) {
+            footerSpans.add(Span.styled(" t ", Theme.hintKey()));
+            footerSpans.add(Span.raw(" toc  "));
+        }
+        if (catalogEntryName != null) {
+            footerSpans.add(Span.styled(" o ", Theme.hintKey()));
+            footerSpans.add(Span.raw(" options "));
+        }
+        Title footer = Title.from(Line.from(footerSpans));
         Block block = Block.builder()
                 .borderType(BorderType.ROUNDED).borders(Borders.ALL)
                 .title(title)
+                .titleBottom(footer)
                 .build();
         if (docLines != null) {
-            frame.renderWidget(block, popup);
-            Rect inner = block.inner(popup);
-            int visibleLines = inner.height();
+            frame.renderWidget(block, area);
+            Rect inner = block.inner(area);
+            List<Rect> hChunks = Layout.horizontal()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(inner);
+            int visibleLines = hChunks.get(0).height();
             int totalLines = docLines.size();
             int clampedScroll = Math.min(docScroll, Math.max(0, totalLines - 
visibleLines));
+            docScroll = clampedScroll;
             int end = Math.min(clampedScroll + visibleLines, totalLines);
             List<Line> visible = new 
ArrayList<>(docLines.subList(clampedScroll, end));
             while (visible.size() < visibleLines) {
@@ -427,15 +531,46 @@ class DocViewerPopup {
             frame.renderWidget(
                     
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new)))
                             .overflow(Overflow.CLIP).build(),
-                    inner);
+                    hChunks.get(0));
+            if (totalLines > visibleLines) {
+                scrollbarState
+                        .contentLength(totalLines)
+                        .viewportContentLength(visibleLines)
+                        .position(clampedScroll);
+                frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+            }
         } else {
+            frame.renderWidget(block, area);
+            Rect inner = block.inner(area);
+            List<Rect> hChunks = Layout.horizontal()
+                    .constraints(Constraint.fill(), Constraint.length(1))
+                    .split(inner);
+            lastContentWidth = hChunks.get(0).width();
+            int viewportHeight = hChunks.get(0).height();
+            if (lastTotalHeight > 0) {
+                int maxScroll = Math.max(0, lastTotalHeight - viewportHeight);
+                if (docScroll > maxScroll) {
+                    docScroll = maxScroll;
+                }
+            }
             MarkdownView view = MarkdownView.builder()
                     .source(docContent)
                     .scroll(docScroll)
-                    .block(block)
                     .styles(Theme.markdownStyles())
                     .build();
-            frame.renderWidget(view, popup);
+            frame.renderWidget(view, hChunks.get(0));
+            int totalHeight = view.computeHeight(lastContentWidth);
+            lastTotalHeight = totalHeight;
+            if (totalHeight > viewportHeight) {
+                scrollbarState
+                        .contentLength(totalHeight)
+                        .viewportContentLength(viewportHeight)
+                        .position(docScroll);
+                frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+            }
+        }
+        if (tocPopup.isVisible()) {
+            tocPopup.render(frame, area);
         }
     }
 
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 e298e7664358..78a42873521e 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
@@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 
 import dev.tamboui.tui.TuiRunner;
+import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 
 /**
@@ -41,6 +42,14 @@ class MonitorContext {
     boolean logPinVisible;
     boolean ratePerMinute;
     BiConsumer<String, Boolean> notificationCallback;
+    BiConsumer<String, String> openMarkdownCallback;
+    OpenOptionsCallback openOptionsCallback;
+    OpenOptionsCallback openCatalogDocCallback;
+
+    @FunctionalInterface
+    interface OpenOptionsCallback {
+        void accept(String name, String kind, CamelCatalog catalog);
+    }
 
     MonitorContext(
                    AtomicReference<List<IntegrationInfo>> data,
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
new file mode 100644
index 000000000000..779232cf7ec8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OptionsViewerPopup.java
@@ -0,0 +1,462 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Overflow;
+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.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.tooling.model.LanguageModel;
+import org.apache.camel.tooling.model.OtherModel;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
+
+class OptionsViewerPopup {
+
+    private boolean visible;
+    private String title;
+    private String currentKind;
+    private CamelCatalog currentCatalog;
+    private String[] tabLabels;
+    private List<List<Line>> tabContent;
+    private int activeTab;
+    private int[] tabScroll;
+    private boolean wantsDoc;
+
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    boolean consumeWantsDoc() {
+        boolean v = wantsDoc;
+        wantsDoc = false;
+        return v;
+    }
+
+    String getCurrentName() {
+        return title;
+    }
+
+    String getCurrentKind() {
+        return currentKind;
+    }
+
+    CamelCatalog getCurrentCatalog() {
+        return currentCatalog;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    void open(String name, String kind, CamelCatalog catalog) {
+        if (catalog == null) {
+            return;
+        }
+        this.title = name;
+        this.currentKind = kind;
+        this.currentCatalog = catalog;
+        this.activeTab = 0;
+        this.wantsDoc = false;
+
+        switch (kind) {
+            case "component" -> openComponent(name, catalog);
+            case "dataformat" -> openDataFormat(name, catalog);
+            case "language" -> openLanguage(name, catalog);
+            case "eip" -> openEip(name, catalog);
+            default -> openOther(name, catalog);
+        }
+
+        if (tabContent == null || tabContent.isEmpty()) {
+            visible = false;
+            return;
+        }
+        tabScroll = new int[tabLabels.length];
+        visible = true;
+    }
+
+    private void openComponent(String name, CamelCatalog catalog) {
+        ComponentModel model = catalog.componentModel(name);
+        if (model == null) {
+            return;
+        }
+        tabLabels = new String[] { "Component", "Endpoint", "Headers" };
+        tabContent = new ArrayList<>();
+        tabContent.add(buildOptionLines(model.getComponentOptions()));
+        tabContent.add(buildOptionLines(model.getEndpointOptions()));
+        tabContent.add(buildHeaderLines(model.getEndpointHeaders()));
+    }
+
+    private void openDataFormat(String name, CamelCatalog catalog) {
+        DataFormatModel model = catalog.dataFormatModel(name);
+        if (model == null) {
+            return;
+        }
+        tabLabels = new String[] { "Options" };
+        tabContent = new ArrayList<>();
+        tabContent.add(buildOptionLines(model.getOptions()));
+    }
+
+    private void openLanguage(String name, CamelCatalog catalog) {
+        LanguageModel model = catalog.languageModel(name);
+        if (model == null) {
+            return;
+        }
+        tabLabels = new String[] { "Options" };
+        tabContent = new ArrayList<>();
+        tabContent.add(buildOptionLines(model.getOptions()));
+    }
+
+    private static final Set<String> EIP_SKIP_OPTIONS = Set.of("input", 
"output", "outputs");
+
+    private void openEip(String name, CamelCatalog catalog) {
+        EipModel model = catalog.eipModel(name);
+        if (model == null) {
+            return;
+        }
+        List<? extends BaseOptionModel> filtered = model.getOptions().stream()
+                .filter(o -> !EIP_SKIP_OPTIONS.contains(o.getName()))
+                .toList();
+        List<Line> optLines = buildOptionLines(filtered);
+        List<Line> propLines = buildOptionLines(model.getExchangeProperties());
+        if (!propLines.isEmpty()) {
+            tabLabels = new String[] { "Options", "Properties" };
+            tabContent = new ArrayList<>();
+            tabContent.add(optLines);
+            tabContent.add(propLines);
+        } else {
+            tabLabels = new String[] { "Options" };
+            tabContent = new ArrayList<>();
+            tabContent.add(optLines);
+        }
+    }
+
+    private void openOther(String name, CamelCatalog catalog) {
+        OtherModel model = catalog.otherModel(name);
+        if (model == null) {
+            return;
+        }
+        tabLabels = new String[] { "Options" };
+        tabContent = new ArrayList<>();
+        tabContent.add(buildOptionLines(model.getOptions()));
+    }
+
+    // --- Option line building ---
+
+    private List<Line> buildOptionLines(List<? extends BaseOptionModel> 
options) {
+        List<Line> lines = new ArrayList<>();
+        if (options == null || options.isEmpty()) {
+            lines.add(Line.from(Span.styled("  No options", 
Style.EMPTY.dim())));
+            return lines;
+        }
+        for (int i = 0; i < options.size(); i++) {
+            if (i > 0) {
+                lines.add(Line.from(""));
+            }
+            BaseOptionModel opt = options.get(i);
+            buildSingleOption(lines, opt);
+        }
+        return lines;
+    }
+
+    private void buildSingleOption(List<Line> lines, BaseOptionModel opt) {
+        List<Span> header = new ArrayList<>();
+        header.add(Span.styled("  " + opt.getName(), 
Style.EMPTY.fg(Theme.accent())));
+
+        String type = opt.getShortJavaType();
+        if (type != null) {
+            header.add(Span.styled(" (" + type + ")", Style.EMPTY.dim()));
+        }
+
+        if (opt.isRequired()) {
+            header.add(Span.styled(" [required]", Theme.warning()));
+        }
+        if (opt.isDeprecated()) {
+            header.add(Span.styled(" [deprecated]", Theme.error()));
+        }
+        if (opt.isSecret()) {
+            header.add(Span.styled(" [secret]", 
Style.EMPTY.fg(Theme.accent()).dim()));
+        }
+
+        lines.add(Line.from(header));
+
+        Object dv = opt.getDefaultValue();
+        if (dv != null) {
+            String dvStr = dv.toString();
+            if (!dvStr.isEmpty()) {
+                lines.add(Line.from(
+                        Span.styled("    Default: ", Style.EMPTY.dim()),
+                        Span.raw(dvStr)));
+            }
+        }
+
+        List<String> enums = opt.getEnums();
+        if (enums != null && !enums.isEmpty()) {
+            String enumStr = String.join(", ", enums);
+            lines.add(Line.from(
+                    Span.styled("    Enum: ", Style.EMPTY.dim()),
+                    Span.raw(enumStr)));
+        }
+
+        String group = opt.getGroup();
+        if (group != null && !group.isEmpty()) {
+            lines.add(Line.from(
+                    Span.styled("    Group: ", Style.EMPTY.dim()),
+                    Span.raw(group)));
+        }
+
+        String desc = opt.getDescription();
+        if (desc != null && !desc.isEmpty()) {
+            lines.add(Line.from(
+                    Span.styled("    ", Style.EMPTY),
+                    Span.styled(desc, Style.EMPTY.dim())));
+        }
+    }
+
+    private List<Line> 
buildHeaderLines(List<ComponentModel.EndpointHeaderModel> headers) {
+        List<Line> lines = new ArrayList<>();
+        if (headers == null || headers.isEmpty()) {
+            lines.add(Line.from(Span.styled("  No headers", 
Style.EMPTY.dim())));
+            return lines;
+        }
+        for (int i = 0; i < headers.size(); i++) {
+            if (i > 0) {
+                lines.add(Line.from(""));
+            }
+            ComponentModel.EndpointHeaderModel h = headers.get(i);
+            buildSingleHeader(lines, h);
+        }
+        return lines;
+    }
+
+    private void buildSingleHeader(List<Line> lines, 
ComponentModel.EndpointHeaderModel h) {
+        List<Span> header = new ArrayList<>();
+        header.add(Span.styled("  " + h.getName(), 
Style.EMPTY.fg(Theme.accent())));
+
+        String type = h.getShortJavaType();
+        if (type != null) {
+            header.add(Span.styled(" (" + type + ")", Style.EMPTY.dim()));
+        }
+
+        String group = h.getGroup();
+        if (group != null && !group.isEmpty()) {
+            header.add(Span.styled("  [" + group + "]", Theme.info()));
+        }
+
+        lines.add(Line.from(header));
+
+        String constant = h.getConstantName();
+        if (constant != null && !constant.isEmpty()) {
+            lines.add(Line.from(
+                    Span.styled("    Constant: ", Style.EMPTY.dim()),
+                    Span.raw(constant)));
+        }
+
+        String desc = h.getDescription();
+        if (desc != null && !desc.isEmpty()) {
+            lines.add(Line.from(
+                    Span.styled("    ", Style.EMPTY),
+                    Span.styled(desc, Style.EMPTY.dim())));
+        }
+    }
+
+    // --- Key handling ---
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+            return true;
+        }
+        if (ke.isCharIgnoreCase('d')) {
+            wantsDoc = true;
+            visible = false;
+            return true;
+        }
+        if (ke.isUp() || ke.isChar('k')) {
+            tabScroll[activeTab] = Math.max(0, tabScroll[activeTab] - 1);
+            return true;
+        }
+        if (ke.isDown() || ke.isChar('j')) {
+            tabScroll[activeTab]++;
+            return true;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            tabScroll[activeTab] = Math.max(0, tabScroll[activeTab] - 10);
+            return true;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            tabScroll[activeTab] += 10;
+            return true;
+        }
+        if (ke.isHome() || ke.isKey(KeyCode.HOME)) {
+            tabScroll[activeTab] = 0;
+            return true;
+        }
+        if (ke.isEnd() || ke.isKey(KeyCode.END)) {
+            tabScroll[activeTab] = Integer.MAX_VALUE;
+            return true;
+        }
+        if (tabLabels.length > 1) {
+            if (ke.isKey(KeyCode.TAB) || ke.isRight()) {
+                activeTab = (activeTab + 1) % tabLabels.length;
+                return true;
+            }
+            if (ke.isLeft()) {
+                activeTab = (activeTab - 1 + tabLabels.length) % 
tabLabels.length;
+                return true;
+            }
+        }
+        return true;
+    }
+
+    boolean handleMouseEvent(MouseEvent me) {
+        if (!visible) {
+            return false;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_UP) {
+            tabScroll[activeTab] = Math.max(0, tabScroll[activeTab] - 3);
+            return true;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+            tabScroll[activeTab] += 3;
+            return true;
+        }
+        return true;
+    }
+
+    // --- Rendering ---
+
+    void render(Frame frame, Rect area) {
+        if (!visible) {
+            return;
+        }
+
+        frame.renderWidget(Clear.INSTANCE, area);
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .title(buildTitle())
+                .titleBottom(buildFooter())
+                .build();
+        frame.renderWidget(block, area);
+
+        Rect inner = block.inner(area);
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        List<Line> content = tabContent.get(activeTab);
+        int visibleLines = hChunks.get(0).height();
+        int totalLines = content.size();
+        int clampedScroll = Math.min(tabScroll[activeTab], Math.max(0, 
totalLines - visibleLines));
+        tabScroll[activeTab] = clampedScroll;
+        int end = Math.min(clampedScroll + visibleLines, totalLines);
+
+        List<Line> visible = new ArrayList<>(content.subList(clampedScroll, 
end));
+        while (visible.size() < visibleLines) {
+            visible.add(Line.from(""));
+        }
+
+        frame.renderWidget(
+                
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new)))
+                        .overflow(Overflow.CLIP).build(),
+                hChunks.get(0));
+
+        if (totalLines > visibleLines) {
+            scrollbarState
+                    .contentLength(totalLines)
+                    .viewportContentLength(visibleLines)
+                    .position(clampedScroll);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+        }
+    }
+
+    private Title buildTitle() {
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.raw(" " + title + " Options  "));
+
+        for (int i = 0; i < tabLabels.length; i++) {
+            if (i > 0) {
+                spans.add(Span.styled(" │ ", Style.EMPTY.dim()));
+            }
+            if (i == activeTab) {
+                spans.add(Span.styled(tabLabels[i], Style.EMPTY.bold()));
+            } else {
+                spans.add(Span.styled(tabLabels[i], Style.EMPTY.dim()));
+            }
+        }
+        spans.add(Span.raw(" "));
+
+        return Title.from(Line.from(spans));
+    }
+
+    private Title buildFooter() {
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.styled(" Esc ", Theme.hintKey()));
+        spans.add(Span.raw(" back  "));
+        spans.add(Span.styled(" ↑↓ ", Theme.hintKey()));
+        spans.add(Span.raw(" scroll  "));
+        if (tabLabels != null && tabLabels.length > 1) {
+            spans.add(Span.styled(" ←→ ", Theme.hintKey()));
+            spans.add(Span.raw(" tab  "));
+        }
+        spans.add(Span.styled(" d ", Theme.hintKey()));
+        spans.add(Span.raw(" doc "));
+        return Title.from(Line.from(spans));
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "Esc", "back");
+        hint(spans, "↑↓", "scroll");
+        if (tabLabels != null && tabLabels.length > 1) {
+            hint(spans, "←→", "tab");
+        }
+        hintLast(spans, "d", "doc");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TocPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TocPopup.java
new file mode 100644
index 000000000000..593092ef638f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TocPopup.java
@@ -0,0 +1,288 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+
+class TocPopup {
+
+    record TocEntry(int level, String title, int charOffset) {
+    }
+
+    private boolean visible;
+    private final FuzzyFilter filter = new FuzzyFilter();
+    private final ListState listState = new ListState();
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+    private List<TocEntry> allEntries;
+    private List<TocEntry> filteredEntries;
+    private Rect popupRect;
+    private TocEntry pendingEntry;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open(List<TocEntry> entries) {
+        this.allEntries = entries;
+        this.pendingEntry = null;
+        this.visible = true;
+        filter.clearFilter();
+        rebuildList();
+    }
+
+    void close() {
+        visible = false;
+        filter.clearFilter();
+    }
+
+    TocEntry consumePendingEntry() {
+        TocEntry e = pendingEntry;
+        pendingEntry = null;
+        return e;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        int size = filteredEntries != null ? filteredEntries.size() : 0;
+        if (ke.isCancel()) {
+            close();
+            return true;
+        }
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            return true;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(size);
+            return true;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            for (int i = 0; i < 5; i++) {
+                listState.selectPrevious();
+            }
+            return true;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            for (int i = 0; i < 5; i++) {
+                listState.selectNext(size);
+            }
+            return true;
+        }
+        if (ke.isHome() || ke.isKey(KeyCode.HOME)) {
+            listState.selectFirst();
+            return true;
+        }
+        if (ke.isEnd() || ke.isKey(KeyCode.END)) {
+            listState.selectLast(size);
+            return true;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = listState.selected();
+            if (sel != null && filteredEntries != null && sel < 
filteredEntries.size()) {
+                pendingEntry = filteredEntries.get(sel);
+                close();
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.BACKSPACE)) {
+            filter.deleteChar();
+            rebuildList();
+            return true;
+        }
+        if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
+            filter.appendChar(ke.string().charAt(0));
+            rebuildList();
+            return true;
+        }
+        return true;
+    }
+
+    boolean handleMouseEvent(MouseEvent me) {
+        if (me.kind() == MouseEventKind.SCROLL_UP) {
+            listState.selectPrevious();
+            return true;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+            int size = filteredEntries != null ? filteredEntries.size() : 0;
+            listState.selectNext(size);
+            return true;
+        }
+        if (me.isClick()) {
+            if (popupRect != null && popupRect.contains(me.x(), me.y())) {
+                int idx = TuiHelper.listItemAt(popupRect, 0,
+                        (filteredEntries != null ? filteredEntries.size() : 0) 
+ 2,
+                        me.x(), me.y());
+                if (idx >= 2 && filteredEntries != null && idx - 2 < 
filteredEntries.size()) {
+                    listState.select(idx - 2);
+                    handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER));
+                }
+                return true;
+            }
+            close();
+            return true;
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (filteredEntries == null) {
+            return;
+        }
+        int popupW = Math.min(80, area.width() - 4);
+        int contentH = filteredEntries.size() + 2;
+        int maxH = area.height() - 4;
+        int popupH = contentH + 2 <= maxH ? contentH + 2 : Math.min(contentH + 
2, maxH);
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+        this.popupRect = popup;
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        String filterText = filter.hasFilter() ? filter.filter() : "";
+        String prompt = "> " + filterText + "█";
+
+        List<ListItem> items = new ArrayList<>();
+        items.add(ListItem.from(Line.from(Span.styled(prompt, Theme.info()))));
+        String sep = "─".repeat(Math.max(1, popupW - 2));
+        items.add(ListItem.from(Line.from(Span.styled(sep, 
Style.EMPTY.dim()))));
+
+        Style normalStyle = Style.EMPTY;
+        Style matchStyle = Theme.label().bold();
+        Style dimStyle = Style.EMPTY.dim();
+        int titleColWidth = popupW - 8;
+
+        for (TocEntry entry : filteredEntries) {
+            List<Span> spans = new ArrayList<>();
+            String indent = entry.level() >= 3 ? "    " : "  ";
+            String prefix = entry.level() >= 3 ? "└ " : "";
+            String title = prefix + entry.title();
+            if (title.length() > titleColWidth) {
+                title = title.substring(0, titleColWidth - 1) + "…";
+            }
+
+            spans.add(Span.raw(indent));
+            if (filter.hasFilter()) {
+                int[] nameMatch = FuzzyFilter.fuzzyMatch(title, 
filter.filter());
+                if (nameMatch != null) {
+                    Line hl = FuzzyFilter.highlightLine(title, nameMatch, 
normalStyle, matchStyle);
+                    spans.addAll(hl.spans());
+                } else {
+                    spans.add(Span.styled(title, normalStyle));
+                }
+            } else {
+                spans.add(Span.styled(title, normalStyle));
+            }
+            items.add(ListItem.from(Line.from(spans)));
+        }
+
+        ListState renderState = new ListState();
+        Integer sel = listState.selected();
+        if (sel != null) {
+            renderState.select(sel + 2);
+        }
+
+        int total = allEntries != null ? allEntries.size() : 0;
+        int shown = filteredEntries.size();
+        String blockTitle = shown == total
+                ? " Table of Contents (" + total + ") "
+                : " Table of Contents (" + shown + "/" + total + ") ";
+
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(blockTitle)
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, renderState);
+
+        int visibleRows = Math.max(1, popup.height() - 2);
+        if (shown + 2 > visibleRows) {
+            scrollbarState
+                    .contentLength(shown)
+                    .viewportContentLength(visibleRows)
+                    .position(sel != null ? sel : 0);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), popup, 
scrollbarState);
+        }
+    }
+
+    private void rebuildList() {
+        if (allEntries == null) {
+            filteredEntries = List.of();
+            return;
+        }
+        if (!filter.hasFilter()) {
+            filteredEntries = new ArrayList<>(allEntries);
+        } else {
+            filteredEntries = new ArrayList<>();
+            String f = filter.filter();
+            for (TocEntry entry : allEntries) {
+                boolean match;
+                if (f.length() <= 2) {
+                    match = entry.title().toLowerCase().contains(f);
+                } else {
+                    match = filter.match(entry.title()) != null;
+                }
+                if (match) {
+                    filteredEntries.add(entry);
+                }
+            }
+        }
+        listState.select(filteredEntries.isEmpty() ? null : 0);
+    }
+
+    static List<TocEntry> extractHeadings(String markdown) {
+        List<TocEntry> entries = new ArrayList<>();
+        if (markdown == null) {
+            return entries;
+        }
+        int offset = 0;
+        for (String line : markdown.split("\n", -1)) {
+            if (line.startsWith("### ")) {
+                entries.add(new TocEntry(3, line.substring(4).trim(), offset));
+            } else if (line.startsWith("## ")) {
+                entries.add(new TocEntry(2, line.substring(3).trim(), offset));
+            }
+            offset += line.length() + 1;
+        }
+        return entries;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 160afbae5523..981ac876bbd5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -32,6 +32,7 @@ import org.apache.camel.tooling.model.BaseModel;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -553,19 +554,23 @@ class TuiToolRegistry {
 
         tools.add(toToolDef(toolDef(
                 "tui_catalog_doc",
-                "Get documentation for a Camel catalog artifact (component, 
data format, language) "
+                "Get documentation for a Camel catalog artifact (component, 
data format, language, EIP) "
                                    + "including description, options, and 
Maven coordinates. "
                                    + "Use optionsFilter to search options by 
keyword (e.g., 'security', 'ssl', 'timeout'). "
                                    + "This enables queries like 'what options 
are there on kafka about security'. "
+                                   + "Set includeDoc=true to get the full 
AsciiDoc documentation for deep-dive questions. "
                                    + "Uses the Camel version from the selected 
integration.",
                 Map.of("name", propDef("string",
-                        "Artifact name (e.g., kafka, json-jackson, simple, 
timer)"),
+                        "Artifact name (e.g., kafka, json-jackson, simple, 
timer, choice, split)"),
                         "kind", propDef("string",
-                                "Artifact kind: component, dataformat, or 
language. "
-                                                  + "If omitted, auto-detects 
by trying component first, then dataformat, then language."),
+                                "Artifact kind: component, dataformat, 
language, or eip. "
+                                                  + "If omitted, auto-detects 
by trying component first, then dataformat, then language, then eip."),
                         "includeOptions", propDef("boolean",
                                 "Whether to include configuration options in 
the response (default: true). "
                                                              + "Set to false 
for a lightweight response with just metadata."),
+                        "includeDoc", propDef("boolean",
+                                "Whether to include the full AsciiDoc 
documentation text in the response (default: false). "
+                                                         + "Useful for 
deep-dive questions about usage, examples, and configuration patterns."),
                         "optionsFilter", propDef("string",
                                 "Filter options by keyword in name or 
description (case-insensitive substring match). "
                                                            + "Only used when 
includeOptions is true.")),
@@ -1622,6 +1627,7 @@ class TuiToolRegistry {
         String kind = args.get("kind") instanceof String v ? v : null;
         String optionsFilter = args.get("optionsFilter") instanceof String v ? 
v : null;
         boolean includeOptions = 
!Boolean.FALSE.equals(args.get("includeOptions"));
+        boolean includeDoc = Boolean.TRUE.equals(args.get("includeDoc"));
 
         String version = facade.getSelectedCamelVersion();
         try {
@@ -1629,7 +1635,7 @@ class TuiToolRegistry {
             if (catalog == null) {
                 return "{\"error\": \"Could not load catalog" + (version != 
null ? " for version " + version : "") + "\"}";
             }
-            return buildCatalogDocResult(catalog, name, kind, optionsFilter, 
includeOptions);
+            return buildCatalogDocResult(catalog, name, kind, optionsFilter, 
includeOptions, includeDoc);
         } catch (Exception e) {
             JsonObject err = new JsonObject();
             err.put("error", "Failed to load catalog: " + e.getMessage());
@@ -1638,13 +1644,15 @@ class TuiToolRegistry {
     }
 
     private String buildCatalogDocResult(
-            CamelCatalog catalog, String name, String kind, String 
optionsFilter, boolean includeOptions) {
+            CamelCatalog catalog, String name, String kind, String 
optionsFilter,
+            boolean includeOptions, boolean includeDoc) {
         String lowerFilter = optionsFilter != null ? 
optionsFilter.toLowerCase() : null;
 
         if (kind == null || "component".equals(kind)) {
             ComponentModel cm = catalog.componentModel(name);
             if (cm != null) {
-                return buildComponentDocJson(cm, lowerFilter, includeOptions);
+                String doc = includeDoc ? catalog.asciiDoc(name + 
"-component") : null;
+                return buildComponentDocJson(cm, lowerFilter, includeOptions, 
doc);
             }
             if (kind != null) {
                 return "{\"error\": \"Component not found: " + name + "\"}";
@@ -1653,7 +1661,8 @@ class TuiToolRegistry {
         if (kind == null || "dataformat".equals(kind)) {
             DataFormatModel dm = catalog.dataFormatModel(name);
             if (dm != null) {
-                return buildDataFormatDocJson(dm, lowerFilter, includeOptions);
+                String doc = includeDoc ? catalog.asciiDoc(name + 
"-dataformat") : null;
+                return buildDataFormatDocJson(dm, lowerFilter, includeOptions, 
doc);
             }
             if (kind != null) {
                 return "{\"error\": \"Data format not found: " + name + "\"}";
@@ -1662,12 +1671,23 @@ class TuiToolRegistry {
         if (kind == null || "language".equals(kind)) {
             LanguageModel lm = catalog.languageModel(name);
             if (lm != null) {
-                return buildLanguageDocJson(lm, lowerFilter, includeOptions);
+                String doc = includeDoc ? catalog.asciiDoc(name + "-language") 
: null;
+                return buildLanguageDocJson(lm, lowerFilter, includeOptions, 
doc);
             }
             if (kind != null) {
                 return "{\"error\": \"Language not found: " + name + "\"}";
             }
         }
+        if (kind == null || "eip".equals(kind)) {
+            EipModel em = catalog.eipModel(name);
+            if (em != null) {
+                String doc = includeDoc ? catalog.asciiDoc(name + "-eip") : 
null;
+                return buildEipDocJson(em, lowerFilter, includeOptions, doc);
+            }
+            if (kind != null) {
+                return "{\"error\": \"EIP not found: " + name + "\"}";
+            }
+        }
         return "{\"error\": \"Artifact not found: " + name + "\"}";
     }
 
@@ -1693,7 +1713,7 @@ class TuiToolRegistry {
         }
     }
 
-    private String buildComponentDocJson(ComponentModel model, String filter, 
boolean includeOptions) {
+    private String buildComponentDocJson(ComponentModel model, String filter, 
boolean includeOptions, String doc) {
         JsonObject result = new JsonObject();
         result.put("kind", "component");
         result.put("name", model.getScheme());
@@ -1731,10 +1751,13 @@ class TuiToolRegistry {
             result.put("options", options);
             result.put("matchedOptions", options.size());
         }
+        if (doc != null) {
+            result.put("doc", doc);
+        }
         return Jsoner.serialize(result);
     }
 
-    private String buildDataFormatDocJson(DataFormatModel model, String 
filter, boolean includeOptions) {
+    private String buildDataFormatDocJson(DataFormatModel model, String 
filter, boolean includeOptions, String doc) {
         JsonObject result = new JsonObject();
         result.put("kind", "dataformat");
         result.put("name", model.getName());
@@ -1759,10 +1782,13 @@ class TuiToolRegistry {
             result.put("options", options);
             result.put("matchedOptions", options.size());
         }
+        if (doc != null) {
+            result.put("doc", doc);
+        }
         return Jsoner.serialize(result);
     }
 
-    private String buildLanguageDocJson(LanguageModel model, String filter, 
boolean includeOptions) {
+    private String buildLanguageDocJson(LanguageModel model, String filter, 
boolean includeOptions, String doc) {
         JsonObject result = new JsonObject();
         result.put("kind", "language");
         result.put("name", model.getName());
@@ -1787,6 +1813,40 @@ class TuiToolRegistry {
             result.put("options", options);
             result.put("matchedOptions", options.size());
         }
+        if (doc != null) {
+            result.put("doc", doc);
+        }
+        return Jsoner.serialize(result);
+    }
+
+    private String buildEipDocJson(EipModel model, String filter, boolean 
includeOptions, String doc) {
+        JsonObject result = new JsonObject();
+        result.put("kind", "eip");
+        result.put("name", model.getName());
+        result.put("title", model.getTitle());
+        result.put("description", model.getDescription());
+        if (model.getLabel() != null) {
+            result.put("label", model.getLabel());
+        }
+        result.put("input", model.isInput());
+        result.put("output", model.isOutput());
+        addCommonModelFields(result, model);
+
+        if (includeOptions) {
+            JsonArray options = new JsonArray();
+            if (model.getOptions() != null) {
+                for (BaseOptionModel opt : model.getOptions()) {
+                    if (matchesOptionFilter(opt, filter)) {
+                        options.add(optionToJson(opt, null));
+                    }
+                }
+            }
+            result.put("options", options);
+            result.put("matchedOptions", options.size());
+        }
+        if (doc != null) {
+            result.put("doc", doc);
+        }
         return Jsoner.serialize(result);
     }
 

Reply via email to