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 e173c4ae3959 chore: camel-jbang - the shell and AI panels can have a 
theme of their own
e173c4ae3959 is described below

commit e173c4ae3959fc8d6147493f34fc913d2a004ab2
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 15 11:00:07 2026 +0200

    chore: camel-jbang - the shell and AI panels can have a theme of their own
    
    The shell (F6) and AI (F8) panels looked like any other focused panel, so
    where the tab ended and the panel started was easy to lose. A new Panel
    Theme setting (camel.tui.panelTheme, by default the main theme) and the
    --panel-theme option draw them in any of the 21 themes, for example Dark
    with a Light AI panel, or a Turbo Pascal shell. The Settings row previews
    the theme while cycling, like the Theme row.
    
    Theme keeps the panel theme as a second palette with its own style engine
    and caches, and resolves every token from it on the drawing thread while
    CamelMonitor draws a panel (Theme.inPanel). The panels compute their colors
    while drawing, so their frame, text, markdown and code blocks follow; the
    tabs, the footer and the dialogs over the screen keep the main theme. The
    base color fill moves from CamelMonitor into Theme (applyBaseColors) and
    runs for the panel area inside that scope.
    
    SettingsPopup steps its choice rows with one helper and draws the AI rows
    in a method of their own, which keeps both methods under the size limit,
    and its tests move to rows by name instead of counting key presses.
    
    chore: camel-jbang - the plugin tests run with a home directory of their own
    
    The tests of the shell and AI panels ran with the developer's home
    directory. AiPanelTest wrote its sample prompts into
    ~/.camel/tui-ai-prompt.history, which keeps the last 100 entries, so each
    test run pushed the developer's own prompts out. The camel-jbang log
    configuration wrote ~/.camel/${sys:pid}.log, as the pid property is only
    set by the launcher, and tests read the developer's .camel-cli.properties.
    
    Surefire now starts the tests with user.home in target/test-home. The
    local Maven repository stays the build's (maven.repo.local), otherwise the
    kamelets catalog a test resolves is downloaded from Maven Central again.
    
    chore: camel-jbang - the AI panel and recording docs mention the panel theme
    
    The AI panel page tells how to set the panel apart from the tab with a
    theme of its own, and the recording tips suggest --panel-theme for demos of
    the AI or shell panel.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/images/jbang/camel-tui-panel-theme.png    | Bin 0 -> 346357 bytes
 .../modules/ROOT/pages/camel-jbang-tui-ai.adoc     |   4 +
 .../ROOT/pages/camel-jbang-tui-recording.adoc      |   4 +-
 .../ROOT/pages/camel-jbang-tui-settings.adoc       |  25 ++-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   4 +
 .../jbang-commands/camel-jbang-tui-monitor.adoc    |   1 +
 .../ROOT/pages/jbang-commands/camel-jbang-tui.adoc |   1 +
 dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml     |  12 ++
 .../camel-jbang-plugin-commands-metadata.json      |   2 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  69 ++++----
 .../dsl/jbang/core/commands/tui/SettingsPopup.java | 181 +++++++++++----------
 .../camel/dsl/jbang/core/commands/tui/Theme.java   | 181 ++++++++++++++++++---
 .../dsl/jbang/core/commands/tui/TuiCommand.java    |  10 ++
 .../dsl/jbang/core/commands/tui/TuiSettings.java   |  13 ++
 .../src/main/resources/tui/help/overview.md        |   5 +-
 .../jbang/core/commands/tui/CamelMonitorTest.java  |  12 +-
 .../core/commands/tui/SettingsPopupRenderTest.java |  23 +--
 .../jbang/core/commands/tui/SettingsPopupTest.java | 136 +++++++---------
 .../jbang/core/commands/tui/SourceSizeTest.java    |   3 +-
 .../dsl/jbang/core/commands/tui/ThemeTest.java     |  80 +++++++++
 .../commands/tui/TuiCommandThemeOptionsTest.java   |  93 +++++++++++
 .../jbang/core/commands/tui/TuiSettingsTest.java   |   2 +
 22 files changed, 618 insertions(+), 243 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-panel-theme.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-panel-theme.png
new file mode 100644
index 000000000000..cdc2b5ba8812
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-panel-theme.png differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
index d39ee1e4be2f..cecff8130bd0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
@@ -34,6 +34,10 @@ via *F2* -> _AI & MCP_ -> _Setup AI_. Use *F2* -> _Settings_ 
to pin a provider,
 (`camel.tui.ai.provider`, `camel.tui.ai.model`, `camel.tui.ai.url`) regardless 
of the environment.
 `camel.tui.ai.acp.command` sets the command line of the custom ACP agent 
(`acp:custom`).
 
+To set the panel apart from the tab under it, give it a theme of its own: 
*Panel Theme* in *F2* -> _Settings_,
+or `--panel-theme=<name>` on the command line, draws the AI and shell panels 
in another of the themes, such as a
+Light panel on the Dark theme (see 
xref:camel-jbang-tui-settings.adoc#_panel_theme[Panel theme]).
+
 image::jbang/camel-tui-ai-panel-answer.png[A local Ollama model explains why 
an order failed]
 
 To run the model on your own machine, with Ollama or an OpenAI-compatible 
server such as LM Studio, see
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-recording.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-recording.adoc
index 5d6ed7eaaa3c..864c46f3318f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-recording.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-recording.adoc
@@ -219,7 +219,9 @@ TIP: Give each demo its own theme, so the GIFs on a page do 
not all look the sam
 theme for that run only and leaves your saved theme as it is, as in
 `camel tui orders --theme=tokyo-night --record=demo.tape`, and a `Theme 
"nord"` line in the tape switches
 it during the recording. Light themes such as `catppuccin-latte` stand out 
between dark ones. See
-xref:camel-jbang-tui-settings.adoc#_theme[Theme] for the list.
+xref:camel-jbang-tui-settings.adoc#_theme[Theme] for the list. In a demo of 
the AI or shell panel,
+`--panel-theme` gives that panel a theme of its own for the run, such as 
`--panel-theme=light` on a dark
+theme, so the viewer sees where the panel starts.
 
 TIP: An AI agent can write and run the tape itself: the commands for the TUI 
are the same as the
 xref:camel-jbang-tui-ai-agents.adoc[MCP tools] of the TUI (`tui_show_caption`, 
`tui_locate` with
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
index cdb90df1c9db..372a7431c1be 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-settings.adoc
@@ -80,7 +80,8 @@ so on -- without leaving the dashboard, and the tabs keep 
updating while the com
 and *↑*/*↓* to recall earlier commands (see <<_input_history>>).
 
 The shell and the AI prompt panel (*F8*) share the same space: opening one 
closes the other. Both
-open at the bottom by default; see *Panel Position* in <<_settings>>.
+open at the bottom by default; see *Panel Position* in <<_settings>>, and 
<<_panel_theme>> to draw
+them in a theme of their own.
 
 == Theme
 
@@ -119,6 +120,23 @@ the bright glow, comments in the darker half intensity 
green.
 Your choice is remembered: it is saved as `camel.tui.theme` in 
`.camel-cli.properties`
 and restored the next time you open the TUI.
 
+=== Panel theme
+
+The shell (*F6*) and AI (*F8*) panels can be drawn in a theme of their own, so 
they stand apart
+from the tab underneath. Set *Panel Theme* in <<_settings>>, which previews 
each theme as you cycle
+through them, or pass `--panel-theme=<name>` on the command line. Everything 
inside the panel takes
+that theme: its frame, the text, the AI answers and their code blocks. The 
tabs, the footer and the
+dialogs that open over the screen keep the main theme, and programs run in the 
shell keep their own
+colors.
+
+A light panel on a dark theme stands out the most, for example Dark with a 
Light panel. Two dark
+themes are calmer: the panel then differs by its accent color and background, 
as with Dark and Tokyo
+Night. The choice is saved as `camel.tui.panelTheme`; `--panel-theme=same` 
draws the panels in the
+main theme for one session.
+
+.Dark with a Light panel theme: the AI panel stands apart from the *Overview* 
tab
+image::jbang/camel-tui-panel-theme.png[The Overview tab in the Dark theme with 
the AI panel below it in the Light theme]
+
 === Theme examples
 
 .Nord: a YAML route in the *Source* tab with the scheme's own code colors
@@ -173,6 +191,11 @@ setting can also be written there by hand:
   so the tab keeps its full height underneath.
 | `move`
 
+| Panel Theme | `camel.tui.panelTheme`
+| The theme those panels are drawn with, so they stand apart from the tabs: 
any of the 21 themes,
+  or the main theme. See <<_panel_theme>>.
+| same as Theme
+
 | Confirm Actions | `camel.tui.confirmActions`
 | Whether destructive actions (quit, stop, restart, stop/start routes) show a 
confirmation dialog
   first. Kill (*X*) always confirms regardless of this setting.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 60062cedc012..75e0a8f269d5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -184,6 +184,10 @@ See the 
xref:camel-jbang-tui-source-editor.adoc#_keyboard_shortcuts[keyboard sho
 | Color theme for this session (e.g., `dark`, `tokyo-night`, `dracula`). See 
xref:camel-jbang-tui-settings.adoc#_theme[Theme] for the full list of 21 
themes. Overrides the persisted `camel.tui.theme` preference when set.
 |
 
+| `--panel-theme`
+| Color theme of the shell (*F6*) and AI (*F8*) panels for this session, or 
`same` for the main theme. See 
xref:camel-jbang-tui-settings.adoc#_panel_theme[Panel Theme]. Overrides the 
persisted `camel.tui.panelTheme` preference when set.
+|
+
 | `--record`
 | Play a `.tape` file in the TUI and record it to an Asciinema `.cast` file. 
See xref:camel-jbang-tui-recording.adoc#_scripted_recording[Scripted Recording] 
for the tape commands.
 |
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
index a2942f5a0dab..23d4719d6a96 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui-monitor.adoc
@@ -23,6 +23,7 @@ camel tui monitor [options]
 | Option | Description | Default | Type
 | `--mcp` | Enable embedded MCP server for AI agent access to the TUI |  | 
boolean
 | `--mcp-port` | MCP server port | 8123 | int
+| `--panel-theme` | Color theme of the shell (F6) and AI (F8) panels, or 
'same' for the main theme (overrides the Panel Theme setting for this session) 
|  | String
 | `--record` | Play a .tape file in the TUI and record it to an Asciinema 
.cast file |  | String
 | `--record-duration` | Maximum duration in milliseconds captured by --record 
| 120000 | int
 | `--record-fps` | Frames per second captured by --record | 10 | int
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
index f716223e2286..436fde02a480 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-tui.adoc
@@ -32,6 +32,7 @@ camel tui [options]
 | Option | Description | Default | Type
 | `--mcp` | Enable embedded MCP server for AI agent access to the TUI |  | 
boolean
 | `--mcp-port` | MCP server port | 8123 | int
+| `--panel-theme` | Color theme of the shell (F6) and AI (F8) panels, or 
'same' for the main theme (overrides the Panel Theme setting for this session) 
|  | String
 | `--record` | Play a .tape file in the TUI and record it to an Asciinema 
.cast file |  | String
 | `--record-duration` | Maximum duration in milliseconds captured by --record 
| 120000 | int
 | `--record-fps` | Frames per second captured by --record | 10 | int
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
index 4f1bfbe20bfa..1add7eddb343 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
@@ -172,6 +172,18 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <systemPropertyVariables>
+                        <!-- the tests write prompt and shell histories, 
settings and the camel-jbang log under the home
+                             directory: a home of their own keeps them out of 
the developer's. The local Maven
+                             repository stays the build's, so what the tests 
resolve is not downloaded again -->
+                        
<user.home>${project.build.directory}/test-home</user.home>
+                        
<maven.repo.local>${settings.localRepository}</maven.repo.local>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
             <plugin>
                 <groupId>org.apache.rat</groupId>
                 <artifactId>apache-rat-plugin</artifactId>
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
index 265a9523e427..ccaaca3c9c10 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
@@ -1,5 +1,5 @@
 {
   "commands": [
-    { "name": "tui", "fullName": "tui", "description": "Camel TUI", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.tui.TuiCommand", 
"plugin": "tui", "options": [ { "names": "--mcp", "description": "Enable 
embedded MCP server for AI agent access to the TUI", "javaType": "boolean", 
"type": "boolean" }, { "names": "--mcp-port", "description": "MCP server port", 
"defaultValue": "8123", "javaType": "int", "type": "integer" }, { "names": 
"--record", "description": "Play a .tape file  [...]
+    { "name": "tui", "fullName": "tui", "description": "Camel TUI", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.tui.TuiCommand", 
"plugin": "tui", "options": [ { "names": "--mcp", "description": "Enable 
embedded MCP server for AI agent access to the TUI", "javaType": "boolean", 
"type": "boolean" }, { "names": "--mcp-port", "description": "MCP server port", 
"defaultValue": "8123", "javaType": "int", "type": "integer" }, { "names": 
"--panel-theme", "description": "Color theme o [...]
   ]
 }
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 834cc9312110..4becf08c8a97 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
@@ -36,8 +36,6 @@ import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
-import dev.tamboui.buffer.Buffer;
-import dev.tamboui.buffer.Cell;
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
@@ -155,6 +153,12 @@ public class CamelMonitor extends CamelCommand {
                         completionCandidates = 
ThemeModeCompletionCandidates.class)
     String theme;
 
+    @CommandLine.Option(names = { "--panel-theme" },
+                        description = "Color theme of the shell (F6) and AI 
(F8) panels, or 'same' for the main theme"
+                                      + " (overrides the Panel Theme setting 
for this session)",
+                        completionCandidates = 
ThemeModeCompletionCandidates.class)
+    String panelTheme;
+
     // State
     private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
     private TabRegistry tabRegistry;
@@ -433,9 +437,9 @@ public class CamelMonitor extends CamelCommand {
     }
 
     /**
-     * Validates and applies the --theme option.
+     * Validates and applies the --theme and --panel-theme options.
      */
-    private void applyThemeOption() {
+    void applyThemeOption() {
         if (theme != null) {
             if (!Theme.isValidMode(theme)) {
                 String expected = String.join("' or '", ThemeMode.ids());
@@ -445,6 +449,16 @@ public class CamelMonitor extends CamelCommand {
             }
             Theme.applyStartupMode(theme);
         }
+        if (panelTheme != null) {
+            if (!Theme.isValidMode(panelTheme) && 
!Theme.PANEL_THEME_SAME.equalsIgnoreCase(panelTheme.strip())) {
+                String expected = String.join("' or '", ThemeMode.ids());
+                throw new CommandLine.ParameterException(
+                        new CommandLine(this),
+                        "Invalid value for option '--panel-theme': expected '" 
+ Theme.PANEL_THEME_SAME + "' or '" + expected
+                                               + "', was '" + panelTheme + 
"'");
+            }
+            Theme.setPanelTheme(panelTheme);
+        }
     }
 
     /**
@@ -1127,7 +1141,12 @@ public class CamelMonitor extends CamelCommand {
         if (ctx.panelOverlay) {
             frame.renderWidget(Clear.INSTANCE, panelArea);
         }
-        panel.accept(frame, panelArea);
+        // with a panel theme (Settings > Panel Theme) the panel and its empty 
cells take that theme's colors, so it
+        // stands apart from the tab; the base colors of the rest of the frame 
are filled at the end of render()
+        Theme.inPanel(() -> {
+            panel.accept(frame, panelArea);
+            Theme.applyBaseColors(frame.buffer(), panelArea);
+        });
         // the draggable border is the panel edge that faces the tab
         panelSplit.setBorderPos(ctx.panelTop ? panelArea.bottom() - 1 : 
panelArea.y());
     }
@@ -1180,6 +1199,10 @@ public class CamelMonitor extends CamelCommand {
         ctx.routeTree = settings.isRouteTree();
         ctx.panelTop = settings.isPanelTop();
         ctx.panelOverlay = settings.isPanelOverlay();
+        if (panelTheme == null) {
+            // --panel-theme wins for the session
+            Theme.setPanelTheme(settings.getPanelTheme());
+        }
     }
 
     // ---- Event Handling ----
@@ -1962,7 +1985,7 @@ public class CamelMonitor extends CamelCommand {
             helpOverlay.render(frame, contentArea);
         }
         renderFooter(frame, mainChunks.get(3));
-        applyThemeBaseColors(frame.buffer(), area);
+        Theme.applyBaseColors(frame.buffer(), area);
         LinkTargets.tidy(frame.buffer());
 
         recordingManager.updateBuffer(frame.buffer());
@@ -2095,7 +2118,7 @@ public class CamelMonitor extends CamelCommand {
 
         int x5 = area.x() + Math.max(0, (area.width() - CharWidth.of(line5)) / 
2);
         frame.buffer().setString(x5, startY + 4, line5, normal);
-        applyThemeBaseColors(frame.buffer(), area);
+        Theme.applyBaseColors(frame.buffer(), area);
     }
 
     private void renderTabs(Frame frame, Rect area) {
@@ -2218,38 +2241,6 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
-    /**
-     * Fills cells that still have no explicit fg/bg after widget rendering. 
TamboUI clears the buffer to
-     * {@link Cell#EMPTY} each frame; when bg is unset the terminal falls back 
to its own default (usually black). Only
-     * patches missing colors so selection highlights, zebra stripes, and 
other widget backgrounds are preserved.
-     */
-    static void applyThemeBaseColors(Buffer buffer, Rect area) {
-        Color baseBg = Theme.baseBg();
-        Color baseFg = Theme.baseFg();
-        Rect intersection = buffer.area().intersection(area);
-        for (int y = intersection.top(); y < intersection.bottom(); y++) {
-            for (int x = intersection.left(); x < intersection.right(); x++) {
-                Cell cell = buffer.get(x, y);
-                if (cell.isContinuation()) {
-                    continue;
-                }
-                Style style = cell.style();
-                Color cellBg = style.bg().orElse(null);
-                Color cellFg = style.fg().orElse(null);
-                if (cellBg == null || cellFg == null) {
-                    Style patch = Style.EMPTY;
-                    if (cellBg == null) {
-                        patch = patch.bg(baseBg);
-                    }
-                    if (cellFg == null) {
-                        patch = patch.fg(baseFg);
-                    }
-                    buffer.set(x, y, cell.patchStyle(patch));
-                }
-            }
-        }
-    }
-
     private void computeTabBadges(String[] badgeTexts, Style[] badgeStyles) {
         Style yellow = Theme.label();
         Style cyan = Style.EMPTY.fg(Theme.accent()).bold();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
index 064160032154..7bfa66ca03ff 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -40,33 +40,38 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
  * Central settings dialog for the Camel TUI. Exposes user preferences (theme, 
starting tab, default run-from-folder)
  * backed by {@link TuiSettings}. Loading, mutating and saving go through a 
single {@code TuiSettings} object so all
  * {@code camel.tui.*} keys are written together. Cycling the theme previews 
it instantly (like {@link ThemePopup}),
- * Enter persists the previewed theme, Esc reverts it; the starting tab is 
applied on the next TUI launch.
+ * Enter persists the previewed theme, Esc reverts it; the panel theme 
previews and reverts the same way. The starting
+ * tab is applied on the next TUI launch.
  */
 class SettingsPopup {
 
-    private static final int ROW_THEME = 0;
-    private static final int ROW_START_TAB = 1;
-    private static final int ROW_SELECT_TAB = 2;
-    private static final int ROW_LOG_PIN = 3;
-    private static final int ROW_RATE_PER = 4;
-    private static final int ROW_PANEL_POSITION = 5;
-    private static final int ROW_PANEL_SPACE = 6;
-    private static final int ROW_CONFIRM_ACTIONS = 7;
-    private static final int ROW_VALIDATE_ON_SAVE = 8;
-    private static final int ROW_ROUTE_TREE = 9;
-    private static final int ROW_FOLDER = 10;
-    private static final int ROW_PROXY_HOST = 11;
-    private static final int ROW_PROXY_PORT = 12;
-    private static final int ROW_SHELL_HISTORY = 13;
-    private static final int ROW_AI_PROVIDER = 14;
-    private static final int ROW_AI_MODEL = 15;
-    private static final int ROW_AI_URL = 16;
-    private static final int ROW_AI_TOOLS = 17;
-    private static final int ROW_AI_OVERVIEW = 18;
-    private static final int ROW_AI_PROMPT_HISTORY = 19;
-    private static final int ROW_AI_ACP_COMMAND = 20;
-    static final int ROW_COUNT = 21;
-    /** Separator lines drawn between the row groups, after rows 2, 6, 10 and 
13. */
+    static final int ROW_THEME = 0;
+    static final int ROW_START_TAB = 1;
+    static final int ROW_SELECT_TAB = 2;
+    static final int ROW_LOG_PIN = 3;
+    static final int ROW_RATE_PER = 4;
+    static final int ROW_PANEL_POSITION = 5;
+    static final int ROW_PANEL_SPACE = 6;
+    static final int ROW_PANEL_THEME = 7;
+    static final int ROW_CONFIRM_ACTIONS = 8;
+    static final int ROW_VALIDATE_ON_SAVE = 9;
+    static final int ROW_ROUTE_TREE = 10;
+    static final int ROW_FOLDER = 11;
+    static final int ROW_PROXY_HOST = 12;
+    static final int ROW_PROXY_PORT = 13;
+    static final int ROW_SHELL_HISTORY = 14;
+    static final int ROW_AI_PROVIDER = 15;
+    static final int ROW_AI_MODEL = 16;
+    static final int ROW_AI_URL = 17;
+    static final int ROW_AI_TOOLS = 18;
+    static final int ROW_AI_OVERVIEW = 19;
+    static final int ROW_AI_PROMPT_HISTORY = 20;
+    static final int ROW_AI_ACP_COMMAND = 21;
+    static final int ROW_COUNT = 22;
+    /**
+     * Separator lines drawn between the row groups, after the Select Tab, 
Panel Theme, Default Folder and Shell History
+     * rows.
+     */
     static final int DIVIDERS = 4;
 
     private static final String[] LOG_PIN_OPTIONS = { "off", "25", "50", "75" 
};
@@ -107,6 +112,10 @@ class SettingsPopup {
     private int ratePerIndex;
     private int panelPositionIndex;
     private int panelSpaceIndex;
+    /** 0 for the main theme, else 1 + the index of the theme in {@link 
ThemeMode#ids()}. */
+    private int panelThemeIndex;
+    /** The panel theme when the dialog opened, restored on Esc after the row 
previewed others. */
+    private String originalPanelTheme;
     private int confirmActionsIndex;
     private int validateOnSaveIndex;
     private int routeTreeIndex;
@@ -189,6 +198,9 @@ class SettingsPopup {
         ratePerIndex = "minutes".equals(currentRatePer) ? 1 : 0;
         panelPositionIndex = settings.isPanelTop() ? 1 : 0;
         panelSpaceIndex = settings.isPanelOverlay() ? 1 : 0;
+        originalPanelTheme = Theme.panelTheme();
+        String currentPanelTheme = settings.getPanelTheme() != null ? 
settings.getPanelTheme() : originalPanelTheme;
+        panelThemeIndex = ThemeMode.ids().indexOf(currentPanelTheme) + 1;
 
         confirmActionsIndex = settings.isConfirmActions() ? 1 : 0;
         validateOnSaveIndex = settings.isValidateOnSave() ? 1 : 0;
@@ -219,6 +231,7 @@ class SettingsPopup {
     void close() {
         if (visible) {
             Theme.revertPreview();
+            Theme.setPanelTheme(originalPanelTheme);
             refreshTheme();
         }
         visible = false;
@@ -230,6 +243,7 @@ class SettingsPopup {
         }
         if (ke.isCancel()) {
             Theme.revertPreview();
+            Theme.setPanelTheme(originalPanelTheme);
             refreshTheme();
             visible = false;
             return true;
@@ -247,13 +261,8 @@ class SettingsPopup {
             return true;
         }
         if (selectedRow == ROW_THEME) {
-            int count = ThemeMode.ids().size();
             int previous = themeIndex;
-            if (ke.isChar(' ') || ke.isRight()) {
-                themeIndex = (themeIndex + 1) % count;
-            } else if (ke.isLeft()) {
-                themeIndex = (themeIndex - 1 + count) % count;
-            }
+            themeIndex = cycle(ke, themeIndex, ThemeMode.ids().size());
             if (themeIndex != previous) {
                 Theme.preview(ThemeMode.ids().get(themeIndex));
                 refreshTheme();
@@ -262,68 +271,52 @@ class SettingsPopup {
         }
         if (selectedRow == ROW_START_TAB) {
             if (!tabNames.isEmpty()) {
-                if (ke.isChar(' ') || ke.isRight()) {
-                    startTabIndex = (startTabIndex + 1) % tabNames.size();
-                } else if (ke.isLeft()) {
-                    startTabIndex = (startTabIndex - 1 + tabNames.size()) % 
tabNames.size();
-                }
+                startTabIndex = cycle(ke, startTabIndex, tabNames.size());
             }
             return true;
         }
         if (selectedRow == ROW_SELECT_TAB) {
             if (!primaryTabNames.isEmpty()) {
-                if (ke.isChar(' ') || ke.isRight()) {
-                    selectTabIndex = (selectTabIndex + 1) % 
primaryTabNames.size();
-                } else if (ke.isLeft()) {
-                    selectTabIndex = (selectTabIndex - 1 + 
primaryTabNames.size()) % primaryTabNames.size();
-                }
+                selectTabIndex = cycle(ke, selectTabIndex, 
primaryTabNames.size());
             }
             return true;
         }
         if (selectedRow == ROW_LOG_PIN) {
-            if (ke.isChar(' ') || ke.isRight()) {
-                logPinIndex = (logPinIndex + 1) % LOG_PIN_OPTIONS.length;
-            } else if (ke.isLeft()) {
-                logPinIndex = (logPinIndex - 1 + LOG_PIN_OPTIONS.length) % 
LOG_PIN_OPTIONS.length;
-            }
+            logPinIndex = cycle(ke, logPinIndex, LOG_PIN_OPTIONS.length);
             return true;
         }
         if (selectedRow == ROW_RATE_PER) {
-            if (ke.isChar(' ') || ke.isRight()) {
-                ratePerIndex = (ratePerIndex + 1) % RATE_PER_OPTIONS.length;
-            } else if (ke.isLeft()) {
-                ratePerIndex = (ratePerIndex - 1 + RATE_PER_OPTIONS.length) % 
RATE_PER_OPTIONS.length;
-            }
+            ratePerIndex = cycle(ke, ratePerIndex, RATE_PER_OPTIONS.length);
             return true;
         }
         if (selectedRow == ROW_PANEL_POSITION) {
-            if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
-                panelPositionIndex = panelPositionIndex == 0 ? 1 : 0;
-            }
+            panelPositionIndex = cycle(ke, panelPositionIndex, 
PANEL_POSITION_OPTIONS.length);
             return true;
         }
         if (selectedRow == ROW_PANEL_SPACE) {
-            if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
-                panelSpaceIndex = panelSpaceIndex == 0 ? 1 : 0;
+            panelSpaceIndex = cycle(ke, panelSpaceIndex, 
PANEL_SPACE_OPTIONS.length);
+            return true;
+        }
+        if (selectedRow == ROW_PANEL_THEME) {
+            int previous = panelThemeIndex;
+            panelThemeIndex = cycle(ke, panelThemeIndex, 
ThemeMode.ids().size() + 1);
+            if (panelThemeIndex != previous) {
+                // previewed at once, like the theme: an open shell or AI 
panel shows it
+                Theme.setPanelTheme(selectedPanelTheme());
+                refreshTheme();
             }
             return true;
         }
         if (selectedRow == ROW_CONFIRM_ACTIONS) {
-            if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
-                confirmActionsIndex = confirmActionsIndex == 0 ? 1 : 0;
-            }
+            confirmActionsIndex = cycle(ke, confirmActionsIndex, 2);
             return true;
         }
         if (selectedRow == ROW_VALIDATE_ON_SAVE) {
-            if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
-                validateOnSaveIndex = validateOnSaveIndex == 0 ? 1 : 0;
-            }
+            validateOnSaveIndex = cycle(ke, validateOnSaveIndex, 2);
             return true;
         }
         if (selectedRow == ROW_ROUTE_TREE) {
-            if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
-                routeTreeIndex = routeTreeIndex == 0 ? 1 : 0;
-            }
+            routeTreeIndex = cycle(ke, routeTreeIndex, 2);
             return true;
         }
         if (selectedRow == ROW_FOLDER) {
@@ -343,11 +336,7 @@ class SettingsPopup {
             return true;
         }
         if (selectedRow == ROW_AI_PROVIDER) {
-            if (ke.isChar(' ') || ke.isRight()) {
-                aiProviderIndex = (aiProviderIndex + 1) % AI_PROVIDERS.size();
-            } else if (ke.isLeft()) {
-                aiProviderIndex = (aiProviderIndex - 1 + AI_PROVIDERS.size()) 
% AI_PROVIDERS.size();
-            }
+            aiProviderIndex = cycle(ke, aiProviderIndex, AI_PROVIDERS.size());
             return true;
         }
         if (selectedRow == ROW_AI_MODEL) {
@@ -359,19 +348,11 @@ class SettingsPopup {
             return true;
         }
         if (selectedRow == ROW_AI_TOOLS) {
-            if (ke.isChar(' ') || ke.isRight()) {
-                aiToolsIndex = (aiToolsIndex + 1) % AI_TOOLS_OPTIONS.length;
-            } else if (ke.isLeft()) {
-                aiToolsIndex = (aiToolsIndex - 1 + AI_TOOLS_OPTIONS.length) % 
AI_TOOLS_OPTIONS.length;
-            }
+            aiToolsIndex = cycle(ke, aiToolsIndex, AI_TOOLS_OPTIONS.length);
             return true;
         }
         if (selectedRow == ROW_AI_OVERVIEW) {
-            if (ke.isChar(' ') || ke.isRight()) {
-                aiOverviewIndex = (aiOverviewIndex + 1) % 
AI_OVERVIEW_OPTIONS.length;
-            } else if (ke.isLeft()) {
-                aiOverviewIndex = (aiOverviewIndex - 1 + 
AI_OVERVIEW_OPTIONS.length) % AI_OVERVIEW_OPTIONS.length;
-            }
+            aiOverviewIndex = cycle(ke, aiOverviewIndex, 
AI_OVERVIEW_OPTIONS.length);
             return true;
         }
         if (selectedRow == ROW_AI_PROMPT_HISTORY) {
@@ -385,6 +366,20 @@ class SettingsPopup {
         return true;
     }
 
+    /**
+     * The value a choice row moves to: Space and Right step forward, Left 
steps back, both wrapping around, so a row of
+     * two values toggles on any of them.
+     */
+    private static int cycle(KeyEvent ke, int index, int count) {
+        if (ke.isChar(' ') || ke.isRight()) {
+            return (index + 1) % count;
+        }
+        if (ke.isLeft()) {
+            return (index - 1 + count) % count;
+        }
+        return index;
+    }
+
     private void save() {
         String selectedThemeId = ThemeMode.ids().get(themeIndex);
         settings.setThemeId(selectedThemeId);
@@ -408,6 +403,9 @@ class SettingsPopup {
             monitorContext.panelTop = panelPositionIndex == 1;
             monitorContext.panelOverlay = panelSpaceIndex == 1;
         }
+        settings.setPanelTheme(selectedPanelTheme());
+        // also when the row was not touched: the saved setting replaces a 
--panel-theme of this session
+        Theme.setPanelTheme(selectedPanelTheme());
         settings.setConfirmActions(confirmActionsIndex == 1 ? "true" : 
"false");
         if (monitorContext != null) {
             monitorContext.confirmActions = confirmActionsIndex == 1;
@@ -516,6 +514,10 @@ class SettingsPopup {
                 selectedRow == ROW_PANEL_SPACE);
         rowY++;
 
+        renderLabel(frame, innerX, rowY, labelW, "Panel Theme:", selectedRow 
== ROW_PANEL_THEME);
+        renderValue(frame, innerX + labelW, rowY, fieldW, panelThemeLabel(), 
selectedRow == ROW_PANEL_THEME);
+        rowY++;
+
         renderDivider(frame, innerX, rowY, innerW);
         rowY++;
 
@@ -562,6 +564,12 @@ class SettingsPopup {
         rowY++;
 
         // --- AI ---
+        renderAiRows(frame, innerX, rowY, labelW, fieldW);
+    }
+
+    /** The AI group, the last rows of the dialog, from line {@code firstY} 
on. */
+    private void renderAiRows(Frame frame, int innerX, int firstY, int labelW, 
int fieldW) {
+        int rowY = firstY;
         renderLabel(frame, innerX, rowY, labelW, "AI Provider:", selectedRow 
== ROW_AI_PROVIDER);
         renderValue(frame, innerX + labelW, rowY, fieldW, 
AI_PROVIDERS.get(aiProviderIndex),
                 selectedRow == ROW_AI_PROVIDER);
@@ -597,7 +605,7 @@ class SettingsPopup {
         if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB || 
selectedRow == ROW_SELECT_TAB
                 || selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER
                 || selectedRow == ROW_PANEL_POSITION || selectedRow == 
ROW_PANEL_SPACE
-                || selectedRow == ROW_CONFIRM_ACTIONS || selectedRow == 
ROW_VALIDATE_ON_SAVE
+                || selectedRow == ROW_PANEL_THEME || selectedRow == 
ROW_CONFIRM_ACTIONS || selectedRow == ROW_VALIDATE_ON_SAVE
                 || selectedRow == ROW_ROUTE_TREE
                 || selectedRow == ROW_AI_PROVIDER || selectedRow == 
ROW_AI_TOOLS || selectedRow == ROW_AI_OVERVIEW) {
             hint(spans, "Space", "cycle");
@@ -671,11 +679,11 @@ class SettingsPopup {
     }
 
     /**
-     * The content line a row is drawn on, counting the dividers that render 
draws after the Select Tab, Panel Space,
+     * The content line a row is drawn on, counting the dividers that render 
draws after the Select Tab, Panel Theme,
      * Default Folder and Shell History rows.
      */
     static int lineOf(int row) {
-        return row + (row > ROW_SELECT_TAB ? 1 : 0) + (row > ROW_PANEL_SPACE ? 
1 : 0) + (row > ROW_FOLDER ? 1 : 0)
+        return row + (row > ROW_SELECT_TAB ? 1 : 0) + (row > ROW_PANEL_THEME ? 
1 : 0) + (row > ROW_FOLDER ? 1 : 0)
                + (row > ROW_SHELL_HISTORY ? 1 : 0);
     }
 
@@ -749,6 +757,15 @@ class SettingsPopup {
         return PANEL_SPACE_OPTIONS[panelSpaceIndex];
     }
 
+    /** The theme id the Panel Theme row has, or {@code null} for the main 
theme. */
+    String selectedPanelTheme() {
+        return panelThemeIndex == 0 ? null : 
ThemeMode.ids().get(panelThemeIndex - 1);
+    }
+
+    private String panelThemeLabel() {
+        return panelThemeIndex == 0 ? "same as Theme" : 
ThemeMode.values()[panelThemeIndex - 1].label();
+    }
+
     String folderText() {
         return folderInput != null ? folderInput.text() : "";
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
index 735f2587b3b2..4242cff6c588 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
@@ -27,8 +27,11 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
 import dev.tamboui.css.Styleable;
 import dev.tamboui.css.engine.StyleEngine;
+import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
 import dev.tamboui.widgets.syntax.SyntaxTheme;
@@ -45,12 +48,18 @@ import org.slf4j.LoggerFactory;
  * Status colors use explicit truecolor hex values in both themes (no ANSI 
names); accent-bg/hint-key/selection keep
  * plain white/black foregrounds. Fallbacks mirror {@code dark.tcss}. If a 
stylesheet is missing or malformed, the
  * facade falls back to the built-in palette below and logs once, so a 
cosmetic failure never crashes the TUI.
+ * <p/>
+ * The shell (F6) and AI (F8) panels can be drawn with a theme of their own, 
the panel theme: {@link #inPanel(Runnable)}
+ * resolves every token from it on the drawing thread while a panel is drawn.
  */
 public final class Theme {
 
     /** Camel brand orange. */
     static final Color ACCENT = Color.rgb(0xF6, 0x91, 0x23);
 
+    /** The {@code --panel-theme} value that draws the shell and AI panels 
with the main theme. */
+    static final String PANEL_THEME_SAME = "same";
+
     private static final Logger LOG = LoggerFactory.getLogger(Theme.class);
 
     private static final String PROP_KEY = "camel.tui.theme";
@@ -113,12 +122,16 @@ public final class Theme {
     private static StyleEngine engine;
     private static ThemeMode mode = ThemeMode.DARK;
     private static ThemeMode previewOriginal;
+    /** The theme of the shell (F6) and AI (F8) panels; null when they are 
drawn with the main theme. */
+    private static volatile Palette panelPalette;
+    /** The palette tokens resolve against on this thread while {@link 
#inPanel(Runnable)} runs. */
+    private static final ThreadLocal<Palette> SCOPE = new ThreadLocal<>();
 
     private Theme() {
     }
 
     public static synchronized Color accent() {
-        StyleEngine e = engine();
+        StyleEngine e = activeEngine();
         if (e == null) {
             return ACCENT;
         }
@@ -135,7 +148,7 @@ public final class Theme {
      * stripes stay readable on both terminals. Applied at the row level so it 
never overrides the selection highlight.
      */
     public static synchronized Color zebra() {
-        StyleEngine e = engine();
+        StyleEngine e = activeEngine();
         if (e == null) {
             return FALLBACK_ZEBRA;
         }
@@ -151,7 +164,7 @@ public final class Theme {
      * Base background color for the main content area. Theme-aware (dark on 
dark mode, white on light mode).
      */
     public static Color baseBg() {
-        StyleEngine e = engine();
+        StyleEngine e = activeEngine();
         if (e == null) {
             return FALLBACK_BASE_BG;
         }
@@ -167,7 +180,7 @@ public final class Theme {
      * Base foreground color for normal text. Theme-aware (light gray on dark 
mode, dark gray on light mode).
      */
     public static Color baseFg() {
-        StyleEngine e = engine();
+        StyleEngine e = activeEngine();
         if (e == null) {
             return FALLBACK_BASE_FG;
         }
@@ -473,12 +486,94 @@ public final class Theme {
         return mode.id();
     }
 
-    /** True if the active mode is dark. */
+    /**
+     * Fills cells that still have no explicit fg/bg after widget rendering 
with {@link #baseBg()} and
+     * {@link #baseFg()}. TamboUI clears the buffer to {@link Cell#EMPTY} each 
frame; when bg is unset the terminal
+     * falls back to its own default (usually black). Only patches missing 
colors so selection highlights, zebra
+     * stripes, and other widget backgrounds are preserved.
+     */
+    static void applyBaseColors(Buffer buffer, Rect area) {
+        Color baseBg = baseBg();
+        Color baseFg = baseFg();
+        Rect intersection = buffer.area().intersection(area);
+        for (int y = intersection.top(); y < intersection.bottom(); y++) {
+            for (int x = intersection.left(); x < intersection.right(); x++) {
+                Cell cell = buffer.get(x, y);
+                if (cell.isContinuation()) {
+                    continue;
+                }
+                Style style = cell.style();
+                Color cellBg = style.bg().orElse(null);
+                Color cellFg = style.fg().orElse(null);
+                if (cellBg == null || cellFg == null) {
+                    Style patch = Style.EMPTY;
+                    if (cellBg == null) {
+                        patch = patch.bg(baseBg);
+                    }
+                    if (cellFg == null) {
+                        patch = patch.fg(baseFg);
+                    }
+                    buffer.set(x, y, cell.patchStyle(patch));
+                }
+            }
+        }
+    }
+
+    /** True if the active mode is dark; inside {@link #inPanel(Runnable)}, if 
the panel theme is. */
     public static boolean isDark() {
+        Palette scoped = SCOPE.get();
+        if (scoped != null) {
+            return !scoped.mode.isLight();
+        }
         engine();
         return !mode.isLight();
     }
 
+    /**
+     * Sets the theme the shell (F6) and AI (F8) panels are drawn with, so 
they stand apart from the tabs. {@code null},
+     * {@code same} or an unknown id draws them with the main theme. Nothing 
is persisted here: the Panel Theme setting
+     * and {@code --panel-theme} decide what is passed.
+     */
+    public static synchronized void setPanelTheme(String modeId) {
+        ThemeMode next = ThemeMode.parse(modeId).orElse(null);
+        Palette current = panelPalette;
+        if (next == null) {
+            panelPalette = null;
+        } else if (current == null || current.mode != next) {
+            panelPalette = new Palette(next, panelEngine(next));
+        }
+    }
+
+    /** The id of the panel theme, or {@code null} when the panels are drawn 
with the main theme. */
+    public static String panelTheme() {
+        Palette palette = panelPalette;
+        return palette != null ? palette.mode.id() : null;
+    }
+
+    /**
+     * Runs the drawing of a panel with the panel theme: every token resolved 
on this thread while it runs comes from
+     * the panel theme, so everything the panel draws (its frame, text, 
markdown and code) takes that theme. Other
+     * threads keep the main theme. Without a panel theme it just runs.
+     */
+    static void inPanel(Runnable drawing) {
+        Palette palette = panelPalette;
+        Palette outer = SCOPE.get();
+        if (palette == null || palette == outer) {
+            drawing.run();
+            return;
+        }
+        SCOPE.set(palette);
+        try {
+            drawing.run();
+        } finally {
+            if (outer != null) {
+                SCOPE.set(outer);
+            } else {
+                SCOPE.remove();
+            }
+        }
+    }
+
     /** Cycle to the next theme in declaration order, persist it (outside test 
mode), and activate it. */
     public static synchronized String toggle() {
         ThemeMode next = mode.next();
@@ -580,6 +675,8 @@ public final class Theme {
         engine = null;
         initialized = false;
         mode = ThemeMode.DARK;
+        panelPalette = null;
+        SCOPE.remove();
     }
 
     private static synchronized void activate(ThemeMode newMode, boolean 
cliOverride) {
@@ -595,19 +692,21 @@ public final class Theme {
     }
 
     private static synchronized Color color(String id, Color fallback) {
-        StyleEngine e = engine();
+        Palette scoped = SCOPE.get();
+        StyleEngine e = scoped != null ? scoped.engine : engine();
         if (e == null) {
             return fallback;
         }
-        Color cached = COLOR_CACHE.get(id);
+        Map<String, Color> cache = scoped != null ? scoped.colors : 
COLOR_CACHE;
+        Color cached = cache.get(id);
         if (cached != null) {
             return cached;
         }
         try {
             // A token the stylesheet does not define resolves to the 
fallback; that is cached too because the
-            // stylesheet cannot change without activate() clearing the cache.
+            // stylesheet cannot change without activate() clearing the cache 
(a panel palette is replaced instead).
             Color resolved = e.resolve(new 
Token(id)).foreground().orElse(fallback);
-            COLOR_CACHE.put(id, resolved);
+            cache.put(id, resolved);
             return resolved;
         } catch (RuntimeException ex) {
             // Not cached: a transient resolution failure must not permanently 
lock this token to the fallback.
@@ -617,17 +716,19 @@ public final class Theme {
     }
 
     private static synchronized Style style(String id, Style fallback) {
-        StyleEngine e = engine();
+        Palette scoped = SCOPE.get();
+        StyleEngine e = scoped != null ? scoped.engine : engine();
         if (e == null) {
             return fallback;
         }
-        Style cached = CACHE.get(id);
+        Map<String, Style> cache = scoped != null ? scoped.styles : CACHE;
+        Style cached = cache.get(id);
         if (cached != null) {
             return cached;
         }
         try {
             Style resolved = e.resolve(new Token(id)).toStyle();
-            CACHE.put(id, resolved);
+            cache.put(id, resolved);
             return resolved;
         } catch (RuntimeException ex) {
             // Not cached: a transient resolution failure must not permanently 
lock this token to the fallback.
@@ -651,17 +752,7 @@ public final class Theme {
             }
         }
         try {
-            StyleEngine e = StyleEngine.create();
-            String stylesheet = mode.id();
-            // Read CSS content via Theme's own classloader instead of 
delegating
-            // to StyleEngine.loadStylesheet() which uses 
StyleEngine.class.getClassLoader().
-            // Under parallel test execution the two classloaders can diverge, 
causing
-            // intermittent "resource not found" failures in CI.
-            String cssContent = loadCssResource(mode.stylesheetResource());
-            e.addStylesheet(stylesheet, cssContent);
-            e.setActiveStylesheet(stylesheet);
-            validateTokens(e, stylesheet);
-            engine = e;
+            engine = createEngine(mode);
         } catch (Exception ex) {
             engine = null;
             logStylesheetFallbackOnce(ex);
@@ -669,6 +760,36 @@ public final class Theme {
         return engine;
     }
 
+    /** The engine tokens resolve against on this thread: the panel theme's 
inside {@link #inPanel}, else the main. */
+    private static StyleEngine activeEngine() {
+        Palette scoped = SCOPE.get();
+        return scoped != null ? scoped.engine : engine();
+    }
+
+    /** The panel theme's engine, or null so its tokens take the built-in 
palette, as the main theme does. */
+    private static StyleEngine panelEngine(ThemeMode themeMode) {
+        try {
+            return createEngine(themeMode);
+        } catch (Exception ex) {
+            logStylesheetFallbackOnce(ex);
+            return null;
+        }
+    }
+
+    private static StyleEngine createEngine(ThemeMode themeMode) throws 
IOException {
+        StyleEngine e = StyleEngine.create();
+        String stylesheet = themeMode.id();
+        // Read CSS content via Theme's own classloader instead of delegating
+        // to StyleEngine.loadStylesheet() which uses 
StyleEngine.class.getClassLoader().
+        // Under parallel test execution the two classloaders can diverge, 
causing
+        // intermittent "resource not found" failures in CI.
+        String cssContent = loadCssResource(themeMode.stylesheetResource());
+        e.addStylesheet(stylesheet, cssContent);
+        e.setActiveStylesheet(stylesheet);
+        validateTokens(e, stylesheet);
+        return e;
+    }
+
     /**
      * Reads a CSS resource from the classpath using this class's own 
classloader, which in the normal Camel
      * class-loading hierarchy has access to project resources. Falls back to 
the thread-context classloader when the
@@ -761,6 +882,20 @@ public final class Theme {
         }
     }
 
+    /** The panel theme: its stylesheet's engine (null when it failed to load) 
and the tokens resolved so far. */
+    private static final class Palette {
+
+        private final ThemeMode mode;
+        private final StyleEngine engine;
+        private final Map<String, Style> styles = new HashMap<>();
+        private final Map<String, Color> colors = new HashMap<>();
+
+        private Palette(ThemeMode mode, StyleEngine engine) {
+            this.mode = mode;
+            this.engine = engine;
+        }
+    }
+
     /** Minimal synthetic {@link Styleable}: an id-only token used for engine 
resolution. */
     private record Token(String id) implements Styleable {
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
index a022f5138711..c4a64b306462 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
@@ -79,6 +79,12 @@ public class TuiCommand extends CamelCommand {
                         completionCandidates = 
ThemeModeCompletionCandidates.class)
     String theme;
 
+    @CommandLine.Option(names = { "--panel-theme" },
+                        description = "Color theme of the shell (F6) and AI 
(F8) panels, or 'same' for the main theme"
+                                      + " (overrides the Panel Theme setting 
for this session)",
+                        completionCandidates = 
ThemeModeCompletionCandidates.class)
+    String panelTheme;
+
     public TuiCommand(CamelJBangMain main, ClassLoader classLoader) {
         super(main);
         this.classLoader = classLoader;
@@ -139,6 +145,10 @@ public class TuiCommand extends CamelCommand {
             args.add("--theme");
             args.add(theme);
         }
+        if (panelTheme != null) {
+            args.add("--panel-theme");
+            args.add(panelTheme);
+        }
         return args;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
index 60742140bfd5..a95d1975a69a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -51,6 +51,7 @@ final class TuiSettings {
     static final String PROP_ROUTE_TREE = "camel.tui.routeTree";
     static final String PROP_PANEL_POSITION = "camel.tui.panelPosition";
     static final String PROP_PANEL_SPACE = "camel.tui.panelSpace";
+    static final String PROP_PANEL_THEME = "camel.tui.panelTheme";
 
     private String themeId;
     private String startTab;
@@ -73,6 +74,7 @@ final class TuiSettings {
     private String routeTree;
     private String panelPosition;
     private String panelSpace;
+    private String panelTheme;
 
     String getThemeId() {
         return themeId;
@@ -282,6 +284,15 @@ final class TuiSettings {
         return "overlay".equalsIgnoreCase(panelSpace);
     }
 
+    /** The theme those panels are drawn with, so they stand apart from the 
tabs; {@code null} for the main theme. */
+    String getPanelTheme() {
+        return panelTheme;
+    }
+
+    void setPanelTheme(String panelTheme) {
+        this.panelTheme = panelTheme;
+    }
+
     /**
      * Loads the current settings, resolving each key with per-key 
local/global precedence via {@link TuiUserConfig}.
      * Unset keys yield {@code null} fields; a read failure yields an object 
with {@code null} fields rather than
@@ -311,6 +322,7 @@ final class TuiSettings {
             settings.routeTree = 
trimToNull(TuiUserConfig.read(PROP_ROUTE_TREE));
             settings.panelPosition = 
trimToNull(TuiUserConfig.read(PROP_PANEL_POSITION));
             settings.panelSpace = 
trimToNull(TuiUserConfig.read(PROP_PANEL_SPACE));
+            settings.panelTheme = 
trimToNull(TuiUserConfig.read(PROP_PANEL_THEME));
         } catch (RuntimeException e) {
             // best-effort: return an object with null fields on read failure
         }
@@ -345,6 +357,7 @@ final class TuiSettings {
             TuiUserConfig.write(PROP_ROUTE_TREE, routeTree);
             TuiUserConfig.write(PROP_PANEL_POSITION, panelPosition);
             TuiUserConfig.write(PROP_PANEL_SPACE, panelSpace);
+            TuiUserConfig.write(PROP_PANEL_THEME, panelTheme);
         } catch (RuntimeException e) {
             // best-effort: a save failure must not disrupt the TUI
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 88ce09f595e6..9005bff65a4d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -132,8 +132,9 @@ Two panels can be opened on top of any tab. Opening one 
closes the other.
   `Ctrl+S` saves or `Esc` discards). Type `/help` for the slash commands
 
 Where the panels open (bottom or top) is configured in Settings
-(`F2` → `Settings...` → `Panel Position`). The tool calls and answers of the 
AI panel
-are recorded in `F2` → `AI & MCP` → `AI Log`.
+(`F2` → `Settings...` → `Panel Position`), and `Panel Theme` draws them in a
+theme of their own, so they stand apart from the tabs. The tool calls and
+answers of the AI panel are recorded in `F2` → `AI & MCP` → `AI Log`.
 
 ## Keys
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
index 49b31945a096..10cb9f58b3f3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java
@@ -133,7 +133,7 @@ class CamelMonitorTest {
         assertEquals(-1, CamelMonitor.footerRegionAt(null, endX, 3), "no 
captured geometry yet");
     }
 
-    // applyThemeBaseColors patches unset terminal cells with the active theme 
canvas colors while
+    // Theme.applyBaseColors patches unset terminal cells with the active 
theme canvas colors while
     // preserving widget-specific foreground and background styles.
 
     @Test
@@ -141,7 +141,7 @@ class CamelMonitorTest {
         Rect area = new Rect(0, 0, 4, 2);
         Buffer buffer = Buffer.empty(area);
 
-        CamelMonitor.applyThemeBaseColors(buffer, area);
+        Theme.applyBaseColors(buffer, area);
 
         Cell cell = buffer.get(0, 0);
         assertEquals(Theme.baseBg(), cell.style().bg().orElse(null), "unset 
cells receive the theme background");
@@ -155,7 +155,7 @@ class CamelMonitorTest {
         Style selection = Theme.selectionBg();
         buffer.setString(1, 0, "X", selection);
 
-        CamelMonitor.applyThemeBaseColors(buffer, area);
+        Theme.applyBaseColors(buffer, area);
 
         Cell cell = buffer.get(1, 0);
         assertEquals(selection.bg().orElse(null), 
cell.style().bg().orElse(null), "selection background is preserved");
@@ -169,7 +169,7 @@ class CamelMonitorTest {
         Color explicitFg = Color.rgb(0xFF, 0x00, 0x00);
         buffer.setString(2, 0, "Y", Style.EMPTY.fg(explicitFg));
 
-        CamelMonitor.applyThemeBaseColors(buffer, area);
+        Theme.applyBaseColors(buffer, area);
 
         Cell cell = buffer.get(2, 0);
         assertEquals(explicitFg, cell.style().fg().orElse(null), "explicit 
foreground is preserved");
@@ -183,7 +183,7 @@ class CamelMonitorTest {
         Color explicitBg = Color.rgb(0x00, 0x00, 0xFF);
         buffer.setString(3, 0, "Z", Style.EMPTY.bg(explicitBg));
 
-        CamelMonitor.applyThemeBaseColors(buffer, area);
+        Theme.applyBaseColors(buffer, area);
 
         Cell cell = buffer.get(3, 0);
         assertEquals(explicitBg, cell.style().bg().orElse(null), "explicit 
background is preserved");
@@ -198,7 +198,7 @@ class CamelMonitorTest {
         Color darkBg = Theme.baseBg();
 
         Theme.setMode("light");
-        CamelMonitor.applyThemeBaseColors(buffer, area);
+        Theme.applyBaseColors(buffer, area);
 
         Cell cell = buffer.get(0, 0);
         assertEquals(Theme.baseBg(), cell.style().bg().orElse(null), "light 
palette background is applied");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
index e592511dd819..4d48d90828e7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
@@ -72,7 +72,7 @@ class SettingsPopupRenderTest {
                 new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6, 
-1)));
         popup.open();
 
-        Rect area = new Rect(0, 0, 80, 29);
+        Rect area = new Rect(0, 0, 80, 30);
         Buffer buffer = Buffer.empty(area);
         Frame frame = Frame.forTesting(buffer);
         popup.render(frame, area);
@@ -83,6 +83,7 @@ class SettingsPopupRenderTest {
         assertTrue(rendered.contains("Starting Tab"), "the Starting Tab row 
should be shown");
         assertTrue(rendered.contains("Log Pin"), "the Log Pin row should be 
shown");
         assertTrue(rendered.contains("Rate per"), "the Rate per row should be 
shown");
+        assertTrue(rendered.contains("Panel Theme"), "the Panel Theme row 
should be shown");
         assertTrue(rendered.contains("Default Folder"), "the Default Folder 
row should be shown");
         assertTrue(rendered.contains("AI Provider"), "the AI Provider row 
should be shown");
         assertTrue(rendered.contains("AI Model"), "the AI Model row should be 
shown");
@@ -111,11 +112,12 @@ class SettingsPopupRenderTest {
 
         // the popup starts two rows down, and its rows below its top border
         int first = 3;
-        assertTrue(lines[first + SettingsPopup.lineOf(0)].contains("Theme:"));
-        assertTrue(lines[first + SettingsPopup.lineOf(9)].contains("Route 
Tree:"));
-        assertTrue(lines[first + SettingsPopup.lineOf(10)].contains("Default 
Folder:"));
-        assertTrue(lines[first + SettingsPopup.lineOf(13)].contains("Shell 
History:"));
-        assertTrue(lines[first + SettingsPopup.lineOf(14)].contains("AI 
Provider:"));
+        assertTrue(lines[first + 
SettingsPopup.lineOf(SettingsPopup.ROW_THEME)].contains("Theme:"));
+        assertTrue(lines[first + 
SettingsPopup.lineOf(SettingsPopup.ROW_PANEL_THEME)].contains("Panel Theme:"));
+        assertTrue(lines[first + 
SettingsPopup.lineOf(SettingsPopup.ROW_ROUTE_TREE)].contains("Route Tree:"));
+        assertTrue(lines[first + 
SettingsPopup.lineOf(SettingsPopup.ROW_FOLDER)].contains("Default Folder:"));
+        assertTrue(lines[first + 
SettingsPopup.lineOf(SettingsPopup.ROW_SHELL_HISTORY)].contains("Shell 
History:"));
+        assertTrue(lines[first + 
SettingsPopup.lineOf(SettingsPopup.ROW_AI_PROVIDER)].contains("AI Provider:"));
         assertTrue(lines[first + SettingsPopup.lineOf(SettingsPopup.ROW_COUNT 
- 1)].contains("ACP Command:"));
     }
 
@@ -126,12 +128,12 @@ class SettingsPopupRenderTest {
         popup.setTabEntries(List.of(new TabRegistry.TabEntry("🐪", "Overview", 
"overview", "1", 0, -1)));
         popup.open();
         Rect area = new Rect(0, 0, 80, 16);
-        for (int i = 0; i < 20; i++) {
+        for (int i = 0; i < SettingsPopup.ROW_AI_ACP_COMMAND; i++) {
             popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
         }
         popup.render(Frame.forTesting(Buffer.empty(area)), area);
         // from ACP Command back up to Default Folder, the row the view 
scrolls up to
-        for (int i = 0; i < 10; i++) {
+        for (int i = 0; i < SettingsPopup.ROW_AI_ACP_COMMAND - 
SettingsPopup.ROW_FOLDER; i++) {
             popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.UP, 
KeyModifiers.NONE));
             popup.render(Frame.forTesting(Buffer.empty(area)), area);
         }
@@ -156,7 +158,7 @@ class SettingsPopupRenderTest {
         String rendered = TuiTestHelper.bufferToString(buffer);
         assertTrue(rendered.contains("Theme:"), rendered);
         assertFalse(rendered.contains("ACP Command"), "the last rows do not 
fit in 25 lines");
-        for (int i = 0; i < 20; i++) {
+        for (int i = 0; i < SettingsPopup.ROW_AI_ACP_COMMAND; i++) {
             popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
         }
         buffer = Buffer.empty(area);
@@ -164,6 +166,7 @@ class SettingsPopupRenderTest {
         rendered = TuiTestHelper.bufferToString(buffer);
         assertTrue(rendered.contains("ACP Command"), rendered);
         assertTrue(rendered.contains("AI History"), rendered);
-        assertFalse(rendered.contains("Theme:"), "the first rows scrolled 
out");
+        // not "Theme:", which the Panel Theme row still shows
+        assertFalse(rendered.contains("Starting Tab:"), "the first rows 
scrolled out");
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
index 23676a64e33f..31233b544ea7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.api.parallel.Isolated;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -74,6 +75,14 @@ class SettingsPopupTest {
         return KeyEvent.ofKey(code, KeyModifiers.NONE);
     }
 
+    /** Moves the selection down to the row with Down, as the user does. */
+    private static void moveTo(SettingsPopup popup, int row) {
+        while (popup.selectedRow() < row) {
+            popup.handleKeyEvent(key(KeyCode.DOWN));
+        }
+        assertThat(popup.selectedRow()).isEqualTo(row);
+    }
+
     @Test
     void openSeedsCurrentValuesFromSettings(@TempDir Path tempDir) throws 
Exception {
         useHome(tempDir);
@@ -133,17 +142,7 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        assertEquals(10, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_FOLDER);
         popup.handleKeyEvent(KeyEvent.ofChar('/'));
         popup.handleKeyEvent(KeyEvent.ofChar('a'));
         assertEquals("/a", popup.folderText());
@@ -156,17 +155,7 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        assertEquals(10, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_FOLDER);
         popup.handleKeyEvent(KeyEvent.ofChar(0x01));
         popup.handleKeyEvent(KeyEvent.ofChar(0x00));
         popup.handleKeyEvent(KeyEvent.ofChar('x'));
@@ -181,21 +170,7 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        popup.handleKeyEvent(key(KeyCode.DOWN));
-        assertEquals(14, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_AI_PROVIDER);
         assertEquals("auto", popup.selectedAiProvider());
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
         assertEquals("ollama", popup.selectedAiProvider());
@@ -227,11 +202,7 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        // navigate to AI Tools (row 16)
-        for (int i = 0; i < 17; i++) {
-            popup.handleKeyEvent(key(KeyCode.DOWN));
-        }
-        assertEquals(17, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_AI_TOOLS);
         assertEquals("auto", popup.selectedAiTools());
 
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
@@ -251,9 +222,7 @@ class SettingsPopupTest {
         reopened.setTabEntries(tabs());
         reopened.open();
         assertEquals("full", reopened.selectedAiTools());
-        for (int i = 0; i < 17; i++) {
-            reopened.handleKeyEvent(key(KeyCode.DOWN));
-        }
+        moveTo(reopened, SettingsPopup.ROW_AI_TOOLS);
         reopened.handleKeyEvent(key(KeyCode.RIGHT));
         assertEquals("auto", reopened.selectedAiTools());
         reopened.handleKeyEvent(key(KeyCode.ENTER));
@@ -267,11 +236,7 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        // navigate to AI Overview (row 17)
-        for (int i = 0; i < 18; i++) {
-            popup.handleKeyEvent(key(KeyCode.DOWN));
-        }
-        assertEquals(18, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_AI_OVERVIEW);
         assertEquals("manual", popup.selectedAiOverview());
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
         assertEquals("auto", popup.selectedAiOverview());
@@ -285,9 +250,7 @@ class SettingsPopupTest {
         reopened.setTabEntries(tabs());
         reopened.open();
         assertEquals("off", reopened.selectedAiOverview());
-        for (int i = 0; i < 18; i++) {
-            reopened.handleKeyEvent(key(KeyCode.DOWN));
-        }
+        moveTo(reopened, SettingsPopup.ROW_AI_OVERVIEW);
         reopened.handleKeyEvent(key(KeyCode.RIGHT));
         assertEquals("manual", reopened.selectedAiOverview());
         reopened.handleKeyEvent(key(KeyCode.ENTER));
@@ -301,21 +264,13 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        // navigate to Shell History (row 10)
-        for (int i = 0; i < 13; i++) {
-            popup.handleKeyEvent(key(KeyCode.DOWN));
-        }
-        assertEquals(13, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_SHELL_HISTORY);
         for (char c : "50".toCharArray()) {
             popup.handleKeyEvent(KeyEvent.ofChar(c));
         }
         assertEquals("50", popup.shellHistoryText());
 
-        // navigate to AI Prompt History (row 18)
-        for (int i = 0; i < 6; i++) {
-            popup.handleKeyEvent(key(KeyCode.DOWN));
-        }
-        assertEquals(19, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_AI_PROMPT_HISTORY);
         for (char c : "200".toCharArray()) {
             popup.handleKeyEvent(KeyEvent.ofChar(c));
         }
@@ -345,15 +300,7 @@ class SettingsPopupTest {
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(KeyEvent.ofChar(' ')); // starting tab Overview 
-> Health
         assertEquals("Health", popup.selectedStartTab());
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // select tab
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // log pin
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // rate per
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // panel position
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // panel space
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // confirm actions
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // validate on save
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // route tree
-        popup.handleKeyEvent(key(KeyCode.DOWN)); // folder
+        moveTo(popup, SettingsPopup.ROW_FOLDER);
         for (char c : "/tmp/p".toCharArray()) {
             popup.handleKeyEvent(KeyEvent.ofChar(c));
         }
@@ -410,10 +357,7 @@ class SettingsPopupTest {
         popup.setTabEntries(tabs());
         popup.open();
 
-        for (int i = 0; i < 5; i++) {
-            popup.handleKeyEvent(key(KeyCode.DOWN));
-        }
-        assertEquals(5, popup.selectedRow());
+        moveTo(popup, SettingsPopup.ROW_PANEL_POSITION);
         assertEquals("bottom", popup.selectedPanelPosition(), "panels open at 
the bottom by default");
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
         assertEquals("top", popup.selectedPanelPosition());
@@ -432,6 +376,46 @@ class SettingsPopupTest {
         assertTrue(persisted.isPanelOverlay());
     }
 
+    @Test
+    void panelThemeRowPreviewsSavesAndRevertsThePanelTheme(@TempDir Path 
tempDir) {
+        useHome(tempDir);
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+
+        moveTo(popup, SettingsPopup.ROW_PANEL_THEME);
+        assertThat(popup.selectedPanelTheme()).as("the panels use the main 
theme by default").isNull();
+        popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertThat(popup.selectedPanelTheme()).isEqualTo("dark");
+        popup.handleKeyEvent(key(KeyCode.RIGHT));
+        assertThat(popup.selectedPanelTheme()).isEqualTo("light");
+        assertThat(Theme.panelTheme()).as("previewed before it is 
saved").isEqualTo("light");
+
+        // Esc puts the panels back as they were
+        popup.handleKeyEvent(key(KeyCode.ESCAPE));
+        assertThat(Theme.panelTheme()).isNull();
+        assertThat(TuiSettings.load().getPanelTheme()).isNull();
+
+        // Enter keeps it and saves it
+        popup.open();
+        moveTo(popup, SettingsPopup.ROW_PANEL_THEME);
+        popup.handleKeyEvent(key(KeyCode.LEFT));
+        assertThat(popup.selectedPanelTheme()).as("Left wraps to the last 
theme").isEqualTo("turbo-pascal");
+        popup.handleKeyEvent(key(KeyCode.ENTER));
+        assertThat(Theme.panelTheme()).isEqualTo("turbo-pascal");
+        
assertThat(TuiSettings.load().getPanelTheme()).isEqualTo("turbo-pascal");
+
+        // after the last theme comes the main theme again, which removes the 
key instead of writing it
+        popup.open();
+        moveTo(popup, SettingsPopup.ROW_PANEL_THEME);
+        assertThat(popup.selectedPanelTheme()).isEqualTo("turbo-pascal");
+        popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertThat(popup.selectedPanelTheme()).isNull();
+        popup.handleKeyEvent(key(KeyCode.ENTER));
+        assertThat(Theme.panelTheme()).isNull();
+        assertThat(TuiSettings.load().getPanelTheme()).isNull();
+    }
+
     @Test
     void aiProviderDropdownOffersAcpAgents() {
         SettingsPopup popup = new SettingsPopup();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index b4dcf6c0f05b..b2b981f21000 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -59,7 +59,7 @@ class SourceSizeTest {
             AiPanel.java 3187
             AiPanel.runAgentLoop 159
             AiUsageStats.render 166
-            CamelMonitor.java 2780
+            CamelMonitor.java 2771
             DiagramDetailSupport.renderEipInfoPanel 159
             DiagramDetailSupport.renderRouteInfoPanel 154
             DiagramSupport.java 2460
@@ -77,7 +77,6 @@ class SourceSizeTest {
             RoutesTab.renderProcessors 179
             SendMessagePopup.doSend 156
             SendMessagePopup.handleKeyEvent 220
-            SettingsPopup.handleKeyEvent 160
             SourceEditAssist.java 2006
             SourceViewer.handleEditKeyEvent 231
             SourceViewer.java 3055
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
index a4d3bf3b521f..320b02ae3a3f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ThemeTest.java
@@ -18,7 +18,12 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -28,6 +33,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.api.parallel.Isolated;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -373,4 +379,78 @@ class ThemeTest {
         assertNotNull(Theme.baseBg());
         assertNotNull(Theme.baseFg());
     }
+
+    @Test
+    void panelThemeAppliesOnlyWhileAPanelDraws() {
+        Theme.setMode("dark");
+        Theme.setPanelTheme("light");
+        assertThat(Theme.panelTheme()).isEqualTo("light");
+        // resolved outside first, so a cached main token must not leak into 
the panel or the other way round
+        Style mainSuccess = Theme.success();
+
+        Color[] colors = new Color[2];
+        Style[] styles = new Style[1];
+        boolean[] dark = new boolean[1];
+        Theme.inPanel(() -> {
+            colors[0] = Theme.baseBg();
+            colors[1] = Theme.accent();
+            styles[0] = Theme.success();
+            dark[0] = Theme.isDark();
+        });
+
+        assertThat(colors[0]).isEqualTo(Color.rgb(0xFF, 0xFF, 0xFF));
+        assertThat(colors[1]).isEqualTo(Color.rgb(0xD9, 0x77, 0x06));
+        assertThat(styles[0]).isEqualTo(Style.EMPTY.fg(Color.rgb(0x22, 0x86, 
0x3A)));
+        assertThat(dark[0]).isFalse();
+        // and the main theme is untouched around it
+        assertThat(Theme.baseBg()).isEqualTo(Color.rgb(0x1E, 0x1E, 0x1E));
+        assertThat(Theme.accent()).isEqualTo(Theme.ACCENT);
+        assertThat(Theme.success()).isEqualTo(mainSuccess);
+        assertThat(Theme.isDark()).isTrue();
+        assertThat(Theme.mode()).isEqualTo("dark");
+    }
+
+    @Test
+    void otherThreadsKeepTheMainThemeWhileAPanelDraws() {
+        Theme.setMode("dark");
+        Theme.setPanelTheme("light");
+        // a thread of its own: CompletableFuture.join() may run a queued task 
on the joining thread itself
+        ExecutorService otherThread = Executors.newSingleThreadExecutor();
+        try {
+            Theme.inPanel(() -> assertThat(otherThread.submit(Theme::baseBg))
+                    .succeedsWithin(Duration.ofSeconds(10))
+                    .isEqualTo(Color.rgb(0x1E, 0x1E, 0x1E)));
+        } finally {
+            otherThread.shutdownNow();
+        }
+    }
+
+    @Test
+    void sameOrUnknownPanelThemeDrawsThePanelsWithTheMainTheme() {
+        Theme.setMode("dark");
+        for (String id : new String[] { null, Theme.PANEL_THEME_SAME, 
"no-such-theme" }) {
+            Theme.setPanelTheme("light");
+            Theme.setPanelTheme(id);
+            assertThat(Theme.panelTheme()).as("panel theme after %s", 
id).isNull();
+            Color[] inside = new Color[1];
+            Theme.inPanel(() -> inside[0] = Theme.baseBg());
+            assertThat(inside[0]).as("panel background after %s", 
id).isEqualTo(Color.rgb(0x1E, 0x1E, 0x1E));
+        }
+    }
+
+    @Test
+    void thePanelAreaTakesThePanelThemeBackground() {
+        // the order CamelMonitor fills a frame in: the panel's area inside 
the panel scope, then the whole frame
+        Theme.setMode("dark");
+        Theme.setPanelTheme("light");
+        Rect frame = new Rect(0, 0, 10, 4);
+        Buffer buffer = Buffer.empty(frame);
+        Rect panel = new Rect(0, 2, 10, 2);
+        Theme.inPanel(() -> Theme.applyBaseColors(buffer, panel));
+        Theme.applyBaseColors(buffer, frame);
+
+        assertThat(buffer.get(0, 1).style().bg()).contains(Color.rgb(0x1E, 
0x1E, 0x1E));
+        assertThat(buffer.get(0, 2).style().bg()).contains(Color.rgb(0xFF, 
0xFF, 0xFF));
+        assertThat(buffer.get(9, 3).style().bg()).contains(Color.rgb(0xFF, 
0xFF, 0xFF));
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommandThemeOptionsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommandThemeOptionsTest.java
new file mode 100644
index 000000000000..d885e66384e2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommandThemeOptionsTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * The {@code --theme} and {@code --panel-theme} options: {@code camel tui} 
forwards them to {@link CamelMonitor}, which
+ * checks and applies them. As with the recording options, an option declared 
on only one of the two commands is either
+ * rejected or silently dropped.
+ */
+@Isolated
+class TuiCommandThemeOptionsTest {
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    @AfterEach
+    void tearDown() {
+        Theme.resetForTesting();
+    }
+
+    @Test
+    void forwardsBothThemeOptions() {
+        assertThat(buildArgs("--theme=dark", "--panel-theme=light"))
+                .containsExactly("--theme", "dark", "--panel-theme", "light");
+        assertThat(buildArgs()).isEmpty();
+    }
+
+    @Test
+    void panelThemeIsAppliedForTheSession() {
+        CamelMonitor monitor = monitor("--panel-theme=tokyo-night");
+        monitor.applyThemeOption();
+        assertThat(Theme.panelTheme()).isEqualTo("tokyo-night");
+    }
+
+    @Test
+    void sameDrawsThePanelsWithTheMainTheme() {
+        Theme.setPanelTheme("light");
+        CamelMonitor monitor = monitor("--panel-theme=same");
+        monitor.applyThemeOption();
+        assertThat(Theme.panelTheme()).isNull();
+    }
+
+    @Test
+    void rejectsAnUnknownPanelTheme() {
+        CamelMonitor monitor = monitor("--panel-theme=neon");
+        assertThatThrownBy(monitor::applyThemeOption)
+                .isInstanceOf(CommandLine.ParameterException.class)
+                .hasMessageContaining("--panel-theme")
+                .hasMessageContaining("'same'")
+                .hasMessageContaining("neon");
+        assertThat(Theme.panelTheme()).isNull();
+    }
+
+    private CamelMonitor monitor(String... args) {
+        CamelMonitor monitor = new CamelMonitor(new CamelJBangMain(), 
getClass().getClassLoader());
+        new CommandLine(monitor).parseArgs(args);
+        return monitor;
+    }
+
+    private List<String> buildArgs(String... args) {
+        TuiCommand command = new TuiCommand(new CamelJBangMain(), 
getClass().getClassLoader());
+        new CommandLine(command).parseArgs(args);
+        return command.buildArgs();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
index 55f3442046ed..4089d53e3b39 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
@@ -69,6 +69,7 @@ class TuiSettingsTest {
         settings.setAiPromptHistory("50");
         settings.setPanelPosition("top");
         settings.setPanelSpace("overlay");
+        settings.setPanelTheme("tokyo-night");
         settings.save();
 
         TuiSettings loaded = TuiSettings.load();
@@ -85,6 +86,7 @@ class TuiSettingsTest {
         assertThat(loaded.getPanelPosition()).isEqualTo("top");
         assertThat(loaded.isPanelTop()).isTrue();
         assertThat(loaded.getPanelSpace()).isEqualTo("overlay");
+        assertThat(loaded.getPanelTheme()).isEqualTo("tokyo-night");
         assertThat(loaded.isPanelOverlay()).isTrue();
     }
 

Reply via email to