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 31c0a7e0fdff CAMEL-23867: camel tui - Add settings dialog
31c0a7e0fdff is described below

commit 31c0a7e0fdff0b6ca3ea391bf6787516d1911aa1
Author: Adriano Machado <[email protected]>
AuthorDate: Thu Jul 9 13:45:18 2026 -0400

    CAMEL-23867: camel tui - Add settings dialog
    
    Closes #24545
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  16 +-
 .../ROOT/pages/camel-jbang-configuration.adoc      |  10 +-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  27 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../dsl/jbang/core/common/CommandLineHelper.java   |  81 +++++-
 .../common/CommandLineHelperMigrationTest.java     | 185 ++++++++++++
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  31 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  25 ++
 .../jbang/core/commands/tui/FolderInputPopup.java  |  40 +--
 .../dsl/jbang/core/commands/tui/SettingsPopup.java | 315 +++++++++++++++++++++
 .../camel/dsl/jbang/core/commands/tui/Theme.java   |  15 +-
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   1 +
 .../dsl/jbang/core/commands/tui/TuiSettings.java   | 101 +++++++
 .../dsl/jbang/core/commands/tui/TuiUserConfig.java |  98 +++++++
 .../src/main/resources/tui/themes/theme.md         |  30 +-
 .../core/commands/tui/FolderInputPopupTest.java    |  88 ++++++
 .../core/commands/tui/SettingsPopupRenderTest.java |  81 ++++++
 .../jbang/core/commands/tui/SettingsPopupTest.java | 226 +++++++++++++++
 .../jbang/core/commands/tui/TuiSettingsTest.java   | 169 +++++++++++
 .../jbang/core/commands/tui/TuiUserConfigTest.java | 173 +++++++++++
 .../infra/cli/services/CliLocalProcessService.java |   2 +-
 21 files changed, 1664 insertions(+), 51 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 0d647fdf0131..ed6963bf53cb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -40,7 +40,21 @@ with empty `nodes`/`edges` arrays if there are no routes to 
connect, so its pres
 the dump has completed (previously it was silently skipped when there were no 
routes).
 
 The Camel TUI (`camel tui`) accepts `--theme=dark` or `--theme=light` to 
choose the color palette at startup.
-When omitted, the persisted `camel.tui.theme` value from 
`.camel-jbang-user.properties` is used.
+When omitted, the persisted `camel.tui.theme` value from 
`.camel-cli.properties` is used.
+
+The Camel TUI now has a *Settings...* entry in the *F2* actions menu for 
choosing the theme, the
+starting tab, and the default run-from-folder. These are stored under 
`camel.tui.*` keys
+(`camel.tui.theme`, `camel.tui.startTab`, `camel.tui.defaultFolder`) in the 
Camel CLI configuration
+file. Each key is read from and written back to the file where it currently 
lives: a key present in
+the local `./camel-cli.properties` stays local (project override), while every 
other key defaults to
+the global `~/.camel-cli.properties`.
+
+The Camel CLI user configuration file has been renamed from 
`camel-jbang-user.properties` to
+`camel-cli.properties` (global `~/.camel-cli.properties`, local 
`./camel-cli.properties`). The dot
+convention is unchanged: the global file is a hidden dotfile, the local file 
is visible. On first
+run the CLI automatically renames a pre-existing global 
`~/.camel-jbang-user.properties` to
+`~/.camel-cli.properties` and a pre-existing local 
`./camel-jbang-user.properties` to
+`./camel-cli.properties`; an existing new file is never overwritten.
 
 === camel-langchain4j-agent
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
index 286e856eed95..f6c9408aa0f6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
@@ -74,11 +74,17 @@ camel run *
 
 Camel CLI uses two configuration files:
 
-- Global: `~/.camel-jbang-user.properties`
-- Local: `./camel-jbang-user.properties` (takes precedence)
+- Global: `~/.camel-cli.properties`
+- Local: `./camel-cli.properties` (takes precedence)
 
 Commands target global by default. Use `--global=false` for local 
configuration.
 
+NOTE: These files were previously named `.camel-jbang-user.properties` 
(global) and
+`camel-jbang-user.properties` (local). On first run the CLI automatically 
renames a pre-existing
+global `~/.camel-jbang-user.properties` to `~/.camel-cli.properties` and a 
pre-existing local
+`./camel-jbang-user.properties` to `./camel-cli.properties`. An existing new 
file is never
+overwritten.
+
 IMPORTANT: A set version via `camel config set` cannot be combined with 
`--camel-version` on the command line.
 
 === Set, unset, list, and get
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 38e0253bc3cb..c8da5fb018fb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -274,6 +274,7 @@ Press *F2* to open the actions menu with quick access to 
common operations:
 | Run Doctor | Check your environment: Java version, JBang, Maven, Docker, 
port conflicts, disk space.
 | Reset Stats | Reset all statistics and metrics for the selected integration.
 | Stop All | Stop running integrations and/or infrastructure services.
+| Settings... | Change the theme, the starting tab, and the default 
run-from-folder.
 | Take Screenshot | Export the current screen as SVG, text, or ANSI art.
 | Start/Stop Tape Recording | Record your session as a `.tape` file for demos.
 |===
@@ -311,14 +312,36 @@ to switch at runtime.
 
 Pass `--theme=dark` or `--theme=light` on the command line to pick the palette 
for
 the current session. The CLI value overrides the persisted preference from
-`.camel-jbang-user.properties`; runtime toggles and the config file still 
apply on
+`.camel-cli.properties`; runtime toggles and the config file still apply on
 later launches when `--theme` is omitted.
 
 The brand orange accent is identical in both themes; status colors (success,
 warning, error) and borders adapt for readability on dark and light terminals.
 
 Your choice is remembered: it is saved as `camel.tui.theme` (`dark` or `light`)
-in `.camel-jbang-user.properties` and restored the next time you open the TUI.
+in `.camel-cli.properties` and restored the next time you open the TUI.
+
+== Settings
+
+Open the *F2* actions menu and choose *Settings...* to change TUI preferences 
in one place:
+
+* *Theme* -- cycle between *dark* and *light* (applied immediately on save).
+* *Starting Tab* -- the tab shown when the TUI launches; any tab (primary or 
under *More*) can be
+  chosen. Defaults to *Overview*.
+* *Default Folder* -- the folder pre-filled in *Run from Folder*. The most 
recently used folder
+  still takes precedence; this default is used only when there is no 
remembered folder.
+
+Use *↑*/*↓* to move between rows, *Space* (or *←*/*→*) to cycle the theme and 
starting tab, type to
+edit the default folder, *Enter* to save, and *Esc* to cancel.
+
+Settings are stored under `camel.tui.*` keys (`camel.tui.theme`, 
`camel.tui.startTab`,
+`camel.tui.defaultFolder`) in the Camel CLI configuration file. Each key is 
read from and
+written back to the file where it currently lives: a key present in the local
+`./camel-cli.properties` is treated as a project-level override and stays 
local, while every
+other key defaults to the global `~/.camel-cli.properties`. This means a 
project can
+deliberately pin a starting tab in its local config without redirecting your 
personal theme
+into the project file. See xref:camel-jbang-configuration.adoc[Configuration] 
for details
+on the global and local files.
 
 == Keyboard Shortcuts
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 53e2bea5afa2..93625b9906df 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -249,6 +249,7 @@ public class CamelJBangMain implements Callable<Integer> {
             return new String[] { v };
         });
 
+        CommandLineHelper.migrateLegacyUserConfig(out);
         CommandLineHelper.augmentWithUserConfiguration(commandLine);
         preExecute(commandLine, args);
         int exitCode = commandLine.execute(args);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
index 6e8b56e5e288..44440aba6077 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
@@ -37,8 +37,10 @@ import picocli.CommandLine;
 public final class CommandLineHelper {
 
     private static volatile Path homeDir = 
Paths.get(HomeHelper.resolveHomeDir());
-    public static final String USER_CONFIG = ".camel-jbang-user.properties";
-    public static final String LOCAL_USER_CONFIG = 
"camel-jbang-user.properties";
+    public static final String USER_CONFIG = ".camel-cli.properties";
+    public static final String LOCAL_USER_CONFIG = "camel-cli.properties";
+    public static final String LEGACY_USER_CONFIG = 
".camel-jbang-user.properties";
+    public static final String LEGACY_LOCAL_USER_CONFIG = 
"camel-jbang-user.properties";
     public static final String CAMEL_DIR = ".camel";
     public static final String CAMEL_JBANG_WORK_DIR = ".camel-jbang";
 
@@ -46,6 +48,79 @@ public final class CommandLineHelper {
         super();
     }
 
+    /**
+     * Migrates any pre-existing legacy user configuration file ({@value 
#LEGACY_USER_CONFIG}) to the new name
+     * ({@value #USER_CONFIG}), for both the user home directory and the 
current working directory.
+     * <p>
+     * The migration is idempotent and best-effort: it renames when only the 
legacy file exists, merges non-conflicting
+     * legacy keys into the current file when the current file is newer or 
same-age, and leaves both files untouched
+     * when the legacy file is newer.
+     */
+    public static void migrateLegacyUserConfig() {
+        migrateLegacyUserConfig(new Printer.SystemOutPrinter());
+    }
+
+    public static void migrateLegacyUserConfig(Printer printer) {
+        migrateLegacyUserConfig(homeDir.resolve(LEGACY_USER_CONFIG), 
homeDir.resolve(USER_CONFIG), printer);
+        migrateLegacyUserConfig(Paths.get(LEGACY_LOCAL_USER_CONFIG), 
Paths.get(LOCAL_USER_CONFIG), printer);
+    }
+
+    private static void migrateLegacyUserConfig(Path legacy, Path current, 
Printer printer) {
+        try {
+            if (!Files.isRegularFile(legacy)) {
+                return;
+            }
+            if (!Files.exists(current)) {
+                Files.move(legacy, current);
+                printer.println("Migrated legacy settings: " + 
legacy.getFileName() + " -> " + current.getFileName());
+                return;
+            }
+
+            long legacyModified = Files.getLastModifiedTime(legacy).toMillis();
+            long currentModified = 
Files.getLastModifiedTime(current).toMillis();
+            if (currentModified < legacyModified) {
+                printer.println("Note: legacy settings file " + 
legacy.getFileName()
+                                + " is newer than " + current.getFileName()
+                                + ". Both files left unchanged. Remove the 
legacy file manually if no longer needed.");
+                return;
+            }
+
+            Properties legacyProps = new OrderedProperties();
+            try (InputStream is = Files.newInputStream(legacy)) {
+                legacyProps.load(is);
+            }
+            Properties currentProps = new OrderedProperties();
+            try (InputStream is = Files.newInputStream(current)) {
+                currentProps.load(is);
+            }
+            boolean merged = false;
+            for (String key : legacyProps.stringPropertyNames()) {
+                if (currentProps.getProperty(key) == null) {
+                    currentProps.setProperty(key, 
legacyProps.getProperty(key));
+                    merged = true;
+                }
+            }
+            if (merged) {
+                try (OutputStream os = Files.newOutputStream(current)) {
+                    currentProps.store(os, null);
+                }
+                printer.println("Merged legacy settings from " + 
legacy.getFileName() + " into " + current.getFileName());
+            }
+            Files.delete(legacy);
+            printer.println("Removed legacy settings file: " + 
legacy.getFileName());
+        } catch (IOException e) {
+            // best-effort: a migration hiccup must never block the CLI
+        }
+    }
+
+    /**
+     * Whether a local user configuration file ({@value #LOCAL_USER_CONFIG}) 
exists in the current working directory.
+     * Callers use this to decide whether to read/write the local file instead 
of the user home file.
+     */
+    public static boolean hasLocalUserConfig() {
+        return Files.exists(Paths.get(LOCAL_USER_CONFIG));
+    }
+
     public static void augmentWithUserConfiguration(CommandLine commandLine) {
         Path file = getUserConfigurationFile();
         if (Files.isRegularFile(file) && Files.exists(file)) {
@@ -167,7 +242,7 @@ public final class CommandLineHelper {
 
         @Override
         public String defaultValue(CommandLine.Model.ArgSpec argSpec) throws 
Exception {
-            // all plugin commands should not support default values from 
camel-jbang-user.properties
+            // all plugin commands should not support default values from the 
user configuration file
             CommandLine.Model.CommandSpec parent = argSpec.command().parent();
             if (parent != null && parent.name().equals("plugin")) {
                 return null;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelperMigrationTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelperMigrationTest.java
new file mode 100644
index 000000000000..e4e8cb996477
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelperMigrationTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the one-time migration of the legacy user configuration file 
({@value CommandLineHelper#LEGACY_USER_CONFIG})
+ * to the current name ({@value CommandLineHelper#USER_CONFIG}). The migration 
must be idempotent, must preserve the
+ * current file on conflicts, and must be a no-op when nothing is present.
+ */
+@Isolated
+class CommandLineHelperMigrationTest {
+
+    private String originalHome;
+
+    @AfterEach
+    void tearDown() {
+        if (originalHome != null) {
+            CommandLineHelper.useHomeDir(originalHome);
+        }
+    }
+
+    private void useHome(Path dir) {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(dir.toString());
+    }
+
+    @Test
+    void renamesLegacyHomeFileToNewName(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+        Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(legacy, "camel.tui.theme=light\n");
+
+        CommandLineHelper.migrateLegacyUserConfig();
+
+        assertFalse(Files.exists(legacy), "legacy file should have been 
renamed away");
+        assertTrue(Files.exists(current), "new file should exist after 
migration");
+        assertEquals("camel.tui.theme=light\n", Files.readString(current), 
"content must be preserved verbatim");
+    }
+
+    @Test
+    void mergesLegacyIntoCurrentWhenCurrentIsNewerOrSameAge(@TempDir Path 
tempDir) throws Exception {
+        useHome(tempDir);
+        Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+        Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(legacy, "camel.tui.theme=light\nold.key=legacy\n");
+        Files.writeString(current, "camel.tui.theme=dark\n");
+
+        CommandLineHelper.migrateLegacyUserConfig();
+
+        assertFalse(Files.exists(legacy), "legacy file should be removed after 
merge");
+        String content = Files.readString(current);
+        assertTrue(content.contains("camel.tui.theme=dark"), "current file 
must win on conflicting keys");
+        assertTrue(content.contains("old.key=legacy"), "legacy-only keys must 
be merged into the current file");
+    }
+
+    @Test
+    void leavesLegacyAloneWhenItIsNewer(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+        Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(current, "camel.tui.theme=dark\n");
+        Files.setLastModifiedTime(current, FileTime.fromMillis(1000));
+        Files.writeString(legacy, "camel.tui.theme=light\nold.key=legacy\n");
+        Files.setLastModifiedTime(legacy, FileTime.fromMillis(5000));
+
+        CommandLineHelper.migrateLegacyUserConfig();
+
+        assertTrue(Files.exists(legacy), "legacy file must remain when it is 
newer");
+        assertTrue(Files.exists(current), "current file must remain when it is 
older");
+        assertEquals("camel.tui.theme=light\nold.key=legacy\n", 
Files.readString(legacy));
+        assertEquals("camel.tui.theme=dark\n", Files.readString(current));
+    }
+
+    @Test
+    void printsDiagnosticMessages(@TempDir Path tempDir) throws Exception {
+        useHome(tempDir);
+        Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+        Files.writeString(legacy, "camel.tui.theme=light\n");
+        CapturingPrinter printer = new CapturingPrinter();
+
+        CommandLineHelper.migrateLegacyUserConfig(printer);
+
+        assertTrue(printer.lines.stream().anyMatch(line -> 
line.contains("Migrated legacy settings")));
+    }
+
+    @Test
+    void noOpWhenNeitherFilePresent(@TempDir Path tempDir) {
+        useHome(tempDir);
+
+        assertDoesNotThrow(() -> CommandLineHelper.migrateLegacyUserConfig());
+        
assertFalse(Files.exists(tempDir.resolve(CommandLineHelper.USER_CONFIG)),
+                "no new file should be created when there is nothing to 
migrate");
+    }
+
+    @Test
+    void renamesLegacyLocalFileToNewName(@TempDir Path tempDir) throws 
Exception {
+        // the historical local file lived in the working directory under the 
visible (no leading dot) name
+        useHome(tempDir);
+        Path legacy = Path.of(CommandLineHelper.LEGACY_LOCAL_USER_CONFIG);
+        Path current = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        Files.deleteIfExists(legacy);
+        Files.deleteIfExists(current);
+        Files.writeString(legacy, "camel.tui.theme=light\n");
+        try {
+            CommandLineHelper.migrateLegacyUserConfig();
+
+            assertFalse(Files.exists(legacy), "legacy local file should have 
been renamed away");
+            assertTrue(Files.exists(current), "new local file should exist 
after migration");
+            assertEquals("camel.tui.theme=light\n", Files.readString(current), 
"content must be preserved verbatim");
+        } finally {
+            Files.deleteIfExists(legacy);
+            Files.deleteIfExists(current);
+        }
+    }
+
+    @Test
+    void isIdempotentOnRepeatedRuns(@TempDir Path tempDir) throws Exception {
+        useHome(tempDir);
+        Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+        Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(legacy, "camel.tui.theme=light\n");
+
+        CommandLineHelper.migrateLegacyUserConfig();
+        CommandLineHelper.migrateLegacyUserConfig();
+
+        assertFalse(Files.exists(legacy));
+        assertTrue(Files.exists(current));
+        assertEquals("camel.tui.theme=light\n", Files.readString(current));
+    }
+
+    private static final class CapturingPrinter implements Printer {
+        private final List<String> lines = new ArrayList<>();
+
+        @Override
+        public void println() {
+            lines.add("");
+        }
+
+        @Override
+        public void println(String line) {
+            lines.add(line);
+        }
+
+        @Override
+        public void print(String output) {
+            lines.add(output);
+        }
+
+        @Override
+        public void printf(String format, Object... args) {
+            lines.add(String.format(format, args));
+        }
+    }
+}
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 eb9ec12a4d07..43da367fb87f 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
@@ -62,6 +62,7 @@ class ActionsPopup {
         STOP_ALL,
         SHELL,
         SCREEN_SUBMENU,
+        SETTINGS,
         MCP_SUBMENU,
         BACK,
         SCREENSHOT,
@@ -112,6 +113,7 @@ class ActionsPopup {
 
     private final GotoTabPopup gotoTabPopup = new GotoTabPopup();
     private final ThemePopup themePopup = new ThemePopup();
+    private final SettingsPopup settingsPopup = new SettingsPopup();
     private final DocViewerPopup docViewerPopup = new DocViewerPopup();
 
     private final ExampleBrowserPopup exampleBrowserPopup;
@@ -212,6 +214,11 @@ class ActionsPopup {
 
     void setGotoTabSupport(List<TabRegistry.TabEntry> entries, Runnable 
callback) {
         gotoTabPopup.setTabEntries(entries, callback);
+        settingsPopup.setTabEntries(entries);
+    }
+
+    void setClearScreenAction(Runnable clearScreen) {
+        settingsPopup.setClearScreen(clearScreen);
     }
 
     void setMcpEnabled(
@@ -267,7 +274,7 @@ class ActionsPopup {
                 Action.SEND_MESSAGE, Action.RUN_EXAMPLE, Action.RUN_FOLDER, 
Action.RUN_INFRA, Action.BROWSE_FILES,
                 Action.STOP_ALL));
         flat.add(null);
-        flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS, 
Action.SCREEN_SUBMENU));
+        flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS, 
Action.SCREEN_SUBMENU, Action.SETTINGS));
         if (mcpEnabled) {
             flat.add(null);
             flat.add(Action.MCP_SUBMENU);
@@ -331,7 +338,7 @@ class ActionsPopup {
     }
 
     boolean isVisible() {
-        return showActionsMenu || themePopup.isVisible() || 
gotoTabPopup.isVisible()
+        return showActionsMenu || themePopup.isVisible() || 
gotoTabPopup.isVisible() || settingsPopup.isVisible()
                 || exampleBrowserPopup.isVisible()
                 || folderInputPopup.isVisible()
                 || runOptionsForm.isVisible()
@@ -411,6 +418,7 @@ class ActionsPopup {
         labels.add("Run Doctor");
         labels.add("Reset Stats");
         labels.add("Screen...");
+        labels.add("Settings...");
         if (mcpEnabled) {
             labels.add("───");
             labels.add("AI & MCP...");
@@ -435,6 +443,7 @@ class ActionsPopup {
         currentSubmenu = null;
         themePopup.close();
         gotoTabPopup.close();
+        settingsPopup.close();
         exampleBrowserPopup.close();
         folderInputPopup.close();
         runOptionsForm.close();
@@ -549,6 +558,13 @@ class ActionsPopup {
             }
             return true;
         }
+        if (settingsPopup.isVisible()) {
+            settingsPopup.handleKeyEvent(ke);
+            if (!settingsPopup.isVisible() && ke.isCancel()) {
+                showActionsMenu = true;
+            }
+            return true;
+        }
         if (showActionsMenu) {
             if (ke.isCancel()) {
                 if (currentSubmenu != null) {
@@ -599,6 +615,9 @@ class ActionsPopup {
                     } else if (action == Action.GOTO_TAB) {
                         showActionsMenu = false;
                         gotoTabPopup.open();
+                    } else if (action == Action.SETTINGS) {
+                        showActionsMenu = false;
+                        settingsPopup.open();
                     } else if (action == Action.SWITCH_INTEGRATION) {
                         if (hasMultipleIntegrations()) {
                             showActionsMenu = false;
@@ -783,6 +802,9 @@ class ActionsPopup {
         if (gotoTabPopup.isVisible()) {
             gotoTabPopup.render(frame, area);
         }
+        if (settingsPopup.isVisible()) {
+            settingsPopup.render(frame, area);
+        }
         if (showActionsMenu) {
             renderActionsMenu(frame, area);
         }
@@ -870,6 +892,10 @@ class ActionsPopup {
             themePopup.renderFooter(spans);
             return;
         }
+        if (settingsPopup.isVisible()) {
+            settingsPopup.renderFooter(spans);
+            return;
+        }
         if (gotoTabPopup.isVisible()) {
             hint(spans, "type", "filter");
             hint(spans, TuiIcons.HINT_SCROLL, "navigate");
@@ -940,6 +966,7 @@ class ActionsPopup {
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCTOR, "Run 
Doctor")));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.RESET, "Reset 
Stats")));
         items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.COMPUTER, 
"Screen...")));
+        items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.SETTINGS, 
"Settings...")));
         // Group 3: MCP (conditional)
         if (mcpEnabled) {
             items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
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 0ebba077758d..fce2f707c15c 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
@@ -488,10 +488,13 @@ public class CamelMonitor extends CamelCommand {
             actionsPopup.setScheduler(tui.scheduler());
             actionsPopup.setResetScreenAction(() -> tui.terminal().clear());
             actionsPopup.setThemeRefreshAction(() -> tui.terminal().clear());
+            actionsPopup.setClearScreenAction(() -> tui.terminal().clear());
             actionsPopup.setCamelAnimationAction(() -> 
playBuiltinAnimation("camel"));
             // Preload diagram data if an integration was auto-selected
             tabRegistry.routesTab().preloadDiagram();
             tabRegistry.diagramTab().preloadDiagram();
+            // Open on the configured starting tab (if any), now that all tab 
wiring is in place
+            applyStartingTab();
             // Intercept Ctrl+C: quit the TUI cleanly instead of letting
             // the JVM tear down the classloader while we're still running
             Signal.handle(new Signal("INT"), sig -> tui.quit());
@@ -510,6 +513,28 @@ public class CamelMonitor extends CamelCommand {
         return 0;
     }
 
+    /**
+     * Selects the tab configured as the starting tab in {@link TuiSettings} 
(if any), resolving the stored programmatic
+     * name against {@link TabRegistry#allTabEntries()}. Unset, unknown, or 
the default "Overview" leaves the initial
+     * {@code TAB_OVERVIEW} selection untouched. Reuses the same selection 
logic as the Go-to callback.
+     */
+    private void applyStartingTab() {
+        String startTab = TuiSettings.load().getStartTab();
+        if (startTab == null || startTab.isBlank() || 
"Overview".equals(startTab)) {
+            return;
+        }
+        for (TabRegistry.TabEntry entry : tabRegistry.allTabEntries()) {
+            if (entry.name().equals(startTab)) {
+                if (entry.moreIndex() >= 0) {
+                    tabRegistry.selectMoreTab(entry.moreIndex());
+                } else {
+                    tabRegistry.handleTabKey(entry.tabIndex(), ctx, 
dataService);
+                }
+                return;
+            }
+        }
+    }
+
     // ---- Event Handling ----
 
     private boolean handleEvent(Event event, TuiRunner runner) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
index 6cd85a92e040..a0f04643c371 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
@@ -37,9 +37,7 @@ import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.input.TextInput;
 import dev.tamboui.widgets.input.TextInputState;
 import dev.tamboui.widgets.paragraph.Paragraph;
-import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
-import org.apache.camel.dsl.jbang.core.common.Printer;
 
 class FolderInputPopup {
 
@@ -99,6 +97,10 @@ class FolderInputPopup {
         return detectedPomPath;
     }
 
+    String getInputText() {
+        return inputState != null ? inputState.text() : null;
+    }
+
     void clearSelection() {
         selectedFolder = null;
         detectedPomPath = null;
@@ -111,10 +113,14 @@ class FolderInputPopup {
     void open() {
         showInput = true;
         String lastFolder = loadLastFolder();
-        if (lastFolder == null) {
-            lastFolder = System.getProperty("user.dir");
+        String initialFolder;
+        if (lastFolder != null) {
+            initialFolder = lastFolder;
+        } else {
+            String defaultFolder = TuiSettings.load().getDefaultFolder();
+            initialFolder = defaultFolder != null ? defaultFolder : 
System.getProperty("user.dir");
         }
-        inputState = new TextInputState(lastFolder != null ? lastFolder : "");
+        inputState = new TextInputState(initialFolder);
         historyIndex = -1;
         folderBrowser.setOnSelect(path -> {
             inputState = new TextInputState(path);
@@ -290,7 +296,10 @@ class FolderInputPopup {
         } else if (ke.isEnd()) {
             inputState.moveCursorToEnd();
         } else if (ke.code() == KeyCode.CHAR) {
-            inputState.insert(ke.string().charAt(0));
+            char ch = ke.string().charAt(0);
+            if (ch >= 0x20 && ch != 0x7F) {
+                inputState.insert(ch);
+            }
         }
     }
 
@@ -362,27 +371,18 @@ class FolderInputPopup {
     }
 
     private static String loadLastFolder() {
-        String[] holder = { null };
         try {
-            CommandLineHelper.loadProperties(props -> {
-                holder[0] = props.getProperty(PROP_LAST_FOLDER);
-            });
+            return TuiUserConfig.read(PROP_LAST_FOLDER);
         } catch (RuntimeException e) {
-            // ignore
+            return null;
         }
-        return holder[0];
     }
 
     private static void persistLastFolder(String folder) {
         try {
-            CommandLineHelper.createPropertyFile(false);
-            CommandLineHelper.loadProperties(props -> {
-                props.setProperty(PROP_LAST_FOLDER, folder);
-                CommandLineHelper.storeProperties(props,
-                        new Printer.QuietPrinter(new 
Printer.SystemOutPrinter()), false);
-            });
-        } catch (Exception e) {
-            // ignore
+            TuiUserConfig.write(PROP_LAST_FOLDER, folder);
+        } catch (RuntimeException e) {
+            // best-effort: a persist failure must not disrupt the TUI
         }
     }
 
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
new file mode 100644
index 000000000000..d601a21f58d7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -0,0 +1,315 @@
+/*
+ * 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.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
+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.
+ */
+class SettingsPopup {
+
+    private static final int ROW_THEME = 0;
+    private static final int ROW_START_TAB = 1;
+    private static final int ROW_FOLDER = 2;
+    private static final int ROW_COUNT = 3;
+
+    private boolean visible;
+    private int selectedRow;
+
+    private TuiSettings settings;
+    private int themeIndex;
+    private int startTabIndex;
+    private TextInputState folderInput;
+    private List<String> tabNames = new ArrayList<>();
+
+    private List<TabRegistry.TabEntry> tabEntries;
+    private Runnable clearScreen;
+
+    void setTabEntries(List<TabRegistry.TabEntry> tabEntries) {
+        this.tabEntries = tabEntries;
+    }
+
+    void setClearScreen(Runnable clearScreen) {
+        this.clearScreen = clearScreen;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open() {
+        settings = TuiSettings.load();
+
+        List<String> themeIds = ThemeMode.ids();
+        String currentTheme = settings.getThemeId() != null ? 
settings.getThemeId() : Theme.mode();
+        themeIndex = Math.max(0, themeIds.indexOf(currentTheme));
+
+        tabNames = new ArrayList<>();
+        if (tabEntries != null) {
+            for (TabRegistry.TabEntry entry : tabEntries) {
+                tabNames.add(entry.name());
+            }
+        }
+        String currentStartTab = settings.getStartTab() != null ? 
settings.getStartTab() : "Overview";
+        int idx = tabNames.indexOf(currentStartTab);
+        startTabIndex = idx >= 0 ? idx : 0;
+
+        folderInput = new TextInputState(settings.getDefaultFolder() != null ? 
settings.getDefaultFolder() : "");
+        selectedRow = ROW_THEME;
+        visible = true;
+    }
+
+    void close() {
+        if (visible) {
+            Theme.revertPreview();
+            refreshTheme();
+        }
+        visible = false;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            Theme.revertPreview();
+            refreshTheme();
+            visible = false;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            save();
+            return true;
+        }
+        if (ke.isUp() || ke.isFocusPrevious()) {
+            selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
+            return true;
+        }
+        if (ke.isDown() || ke.isFocusNext()) {
+            selectedRow = (selectedRow + 1) % ROW_COUNT;
+            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;
+            }
+            if (themeIndex != previous) {
+                Theme.preview(ThemeMode.ids().get(themeIndex));
+                refreshTheme();
+            }
+            return true;
+        }
+        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();
+                }
+            }
+            return true;
+        }
+        if (selectedRow == ROW_FOLDER) {
+            handleTextInput(ke, folderInput);
+            return true;
+        }
+        return true;
+    }
+
+    private void save() {
+        String selectedThemeId = ThemeMode.ids().get(themeIndex);
+        settings.setThemeId(selectedThemeId);
+        if (!tabNames.isEmpty()) {
+            settings.setStartTab(tabNames.get(startTabIndex));
+        }
+        
settings.setDefaultFolder(stripControlChars(folderInput.text().trim()));
+        settings.save();
+        if (Theme.mode().equals(selectedThemeId)) {
+            // Already active via live preview (or unchanged): just persist 
and clear the preview marker.
+            Theme.confirmPreview();
+        } else {
+            // Row was never touched but disk/session state disagrees (e.g. a 
--theme CLI override); force it.
+            Theme.applyStartupMode(selectedThemeId);
+        }
+        refreshTheme();
+        visible = false;
+    }
+
+    void render(Frame frame, Rect area) {
+        int popupW = Math.min(64, area.width() - 4);
+        int popupH = 2 + ROW_COUNT;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .title(" Settings ")
+                .build();
+        frame.renderWidget(block, popup);
+
+        int innerX = popup.left() + 2;
+        int innerW = popup.width() - 4;
+        int labelW = 16;
+        int fieldW = innerW - labelW;
+        int rowY = popup.top() + 1;
+
+        renderLabel(frame, innerX, rowY, labelW, "Theme:", selectedRow == 
ROW_THEME);
+        renderValue(frame, innerX + labelW, rowY, fieldW, 
ThemeMode.values()[themeIndex].label(), selectedRow == ROW_THEME);
+        rowY++;
+
+        renderLabel(frame, innerX, rowY, labelW, "Starting Tab:", selectedRow 
== ROW_START_TAB);
+        renderValue(frame, innerX + labelW, rowY, fieldW, currentTabLabel(), 
selectedRow == ROW_START_TAB);
+        rowY++;
+
+        renderLabel(frame, innerX, rowY, labelW, "Default Folder:", 
selectedRow == ROW_FOLDER);
+        renderFolder(frame, innerX + labelW, rowY, fieldW, selectedRow == 
ROW_FOLDER);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+        if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB) {
+            hint(spans, "Space", "cycle");
+        }
+        hint(spans, "Enter", "save");
+        hintLast(spans, "Esc", "cancel");
+    }
+
+    // ---- Helpers ----
+
+    private void refreshTheme() {
+        if (clearScreen != null) {
+            clearScreen.run();
+        }
+    }
+
+    private String currentTabLabel() {
+        if (tabEntries == null || tabEntries.isEmpty()) {
+            return "Overview";
+        }
+        TabRegistry.TabEntry entry = tabEntries.get(Math.min(startTabIndex, 
tabEntries.size() - 1));
+        return entry.icon() + " " + entry.name();
+    }
+
+    private void handleTextInput(KeyEvent ke, TextInputState active) {
+        if (ke.isDeleteBackward()) {
+            active.deleteBackward();
+        } else if (ke.isDeleteForward()) {
+            active.deleteForward();
+        } else if (ke.isLeft()) {
+            active.moveCursorLeft();
+        } else if (ke.isRight()) {
+            active.moveCursorRight();
+        } else if (ke.isHome()) {
+            active.moveCursorToStart();
+        } else if (ke.isEnd()) {
+            active.moveCursorToEnd();
+        } else if (ke.code() == KeyCode.CHAR) {
+            char ch = ke.string().charAt(0);
+            if (ch >= 0x20 && ch != 0x7F) {
+                active.insert(ch);
+            }
+        }
+    }
+
+    private static String stripControlChars(String s) {
+        if (s == null) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); i++) {
+            char ch = s.charAt(i);
+            if (ch >= 0x20 && ch != 0x7F) {
+                sb.append(ch);
+            }
+        }
+        return sb.toString();
+    }
+
+    private void renderLabel(Frame frame, int x, int y, int w, String label, 
boolean selected) {
+        Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim();
+        frame.renderWidget(Paragraph.from(Line.from(Span.styled(label, 
style))), new Rect(x, y, w, 1));
+    }
+
+    private void renderValue(Frame frame, int x, int y, int w, String text, 
boolean selected) {
+        Style style = selected ? Style.EMPTY.bold() : Style.EMPTY;
+        frame.renderWidget(Paragraph.from(Line.from(Span.styled("[" + text + 
"]", style))), new Rect(x, y, w, 1));
+    }
+
+    private void renderFolder(Frame frame, int x, int y, int w, boolean 
active) {
+        Rect area = new Rect(x, y, w, 1);
+        if (active) {
+            TextInput textInput = TextInput.builder()
+                    .cursorStyle(Style.EMPTY.reversed())
+                    .placeholder("/path/to/folder")
+                    .build();
+            frame.renderStatefulWidget(textInput, area, folderInput);
+        } else {
+            String text = folderInput.text();
+            Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(text.isEmpty() ? "(none)" : text, style))), 
area);
+        }
+    }
+
+    // ---- Test accessors ----
+
+    int selectedRow() {
+        return selectedRow;
+    }
+
+    String selectedThemeId() {
+        return ThemeMode.ids().get(themeIndex);
+    }
+
+    String selectedStartTab() {
+        return tabNames.isEmpty() ? null : tabNames.get(startTabIndex);
+    }
+
+    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 2cfa6dc25ee0..a91368c54a89 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
@@ -31,8 +31,6 @@ import dev.tamboui.css.Styleable;
 import dev.tamboui.css.engine.StyleEngine;
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
-import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
-import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -524,9 +522,7 @@ public final class Theme {
 
     private static boolean tryLoadPersistedMode(ThemeMode[] result) {
         try {
-            CommandLineHelper.loadProperties(props -> {
-                ThemeMode.parse(props.getProperty(PROP_KEY)).ifPresent(parsed 
-> result[0] = parsed);
-            });
+            ThemeMode.parse(TuiUserConfig.read(PROP_KEY)).ifPresent(parsed -> 
result[0] = parsed);
             return true;
         } catch (RuntimeException ex) {
             logPersistedReadFallbackOnce(ex);
@@ -536,13 +532,8 @@ public final class Theme {
 
     private static void persist(ThemeMode newMode) {
         try {
-            CommandLineHelper.createPropertyFile(false);
-            CommandLineHelper.loadProperties(props -> {
-                props.setProperty(PROP_KEY, newMode.id());
-                CommandLineHelper.storeProperties(props,
-                        new Printer.QuietPrinter(new 
Printer.SystemOutPrinter()), false);
-            });
-        } catch (Exception ex) {
+            TuiUserConfig.write(PROP_KEY, newMode.id());
+        } catch (RuntimeException ex) {
             logPersistedWriteFallbackOnce(ex);
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 6c89c4ce9f46..048d61f1ae0a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -86,6 +86,7 @@ final class TuiIcons {
     static final String KEY = "🔑";
     static final String TIP = "💡";
     static final String COMPUTER = "💻";
+    static final String SETTINGS = "🧰";
     static final String HELP = "❔";
     static final String THINKING = "🤔";
 
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
new file mode 100644
index 000000000000..784bd4fb196e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+/**
+ * User-facing preferences for the Camel TUI, persisted under {@code 
camel.tui.*} keys in the CLI user configuration
+ * files. The object is loaded as a whole ({@link #load()}), mutated in 
memory, and written back ({@link #save()}),
+ * giving every setting one place to live and future settings a natural home.
+ * <p>
+ * Persistence goes through {@link TuiUserConfig}, which applies per-key 
precedence between the local and global config
+ * files: a key a project has deliberately committed to the local file is read 
from and written back to the local file,
+ * while a user's personal preferences default to the global home file.
+ */
+final class TuiSettings {
+
+    static final String PROP_THEME = "camel.tui.theme";
+    static final String PROP_START_TAB = "camel.tui.startTab";
+    static final String PROP_DEFAULT_FOLDER = "camel.tui.defaultFolder";
+
+    private String themeId;
+    private String startTab;
+    private String defaultFolder;
+
+    String getThemeId() {
+        return themeId;
+    }
+
+    void setThemeId(String themeId) {
+        this.themeId = themeId;
+    }
+
+    String getStartTab() {
+        return startTab;
+    }
+
+    void setStartTab(String startTab) {
+        this.startTab = startTab;
+    }
+
+    String getDefaultFolder() {
+        return defaultFolder;
+    }
+
+    void setDefaultFolder(String defaultFolder) {
+        this.defaultFolder = defaultFolder;
+    }
+
+    /**
+     * 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
+     * throwing, matching the best-effort behavior elsewhere in the TUI.
+     */
+    static TuiSettings load() {
+        TuiSettings settings = new TuiSettings();
+        try {
+            settings.themeId = trimToNull(TuiUserConfig.read(PROP_THEME));
+            settings.startTab = trimToNull(TuiUserConfig.read(PROP_START_TAB));
+            settings.defaultFolder = 
trimToNull(TuiUserConfig.read(PROP_DEFAULT_FOLDER));
+        } catch (RuntimeException e) {
+            // best-effort: return an object with null fields on read failure
+        }
+        return settings;
+    }
+
+    /**
+     * Writes every field back, routing each key to its home file (local when 
it is a project override, otherwise the
+     * global file) via {@link TuiUserConfig}. A blank/{@code null} field 
removes its key so the file never carries
+     * empty values. Failures are swallowed so a save hiccup never disrupts 
the TUI.
+     */
+    void save() {
+        try {
+            TuiUserConfig.write(PROP_THEME, themeId);
+            TuiUserConfig.write(PROP_START_TAB, startTab);
+            TuiUserConfig.write(PROP_DEFAULT_FOLDER, defaultFolder);
+        } catch (RuntimeException e) {
+            // best-effort: a save failure must not disrupt the TUI
+        }
+    }
+
+    private static String trimToNull(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
new file mode 100644
index 000000000000..5017040e0079
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+
+/**
+ * Central read/write for the Camel TUI's per-user preferences ({@code 
camel.tui.*}) across the Camel CLI configuration
+ * files, applying <em>per-key</em> precedence between the local ({@code 
./camel-cli.properties}) and global
+ * ({@code ~/.camel-cli.properties}) files.
+ * <p>
+ * A key's home is the file where it currently lives: a key present in the 
local file is treated as a project-level
+ * override and is both read from and written back to the local file; every 
other key is personal state and is read from
+ * and written to the global file. This keeps a project's deliberately 
committed overrides (for example a pinned
+ * starting tab) intact, while never redirecting a user's personal preferences 
(theme, last folder, ...) into an
+ * unrelated project's config just because a local file happens to exist for 
other CLI settings.
+ * <p>
+ * Note: if the same key exists in <em>both</em> files (an unusual hand-edited 
state), the local copy wins for reads and
+ * is the one updated on writes; the stale global copy is left as-is.
+ */
+final class TuiUserConfig {
+
+    private TuiUserConfig() {
+    }
+
+    /**
+     * Reads a single {@code camel.tui.*} key. Returns the local value when 
the key is present in the local file,
+     * otherwise the global value, otherwise {@code null}. Propagates a {@link 
RuntimeException} on an I/O failure so
+     * the caller can decide whether to fall back or log.
+     */
+    static String read(String key) {
+        boolean local = livesInLocal(key);
+        String[] holder = { null };
+        CommandLineHelper.loadProperties(props -> holder[0] = 
props.getProperty(key), local);
+        return holder[0];
+    }
+
+    /**
+     * Writes a single {@code camel.tui.*} key back to its home file (the 
local file when the key is already a local
+     * override, otherwise the global file). A blank or {@code null} value 
removes the key. Other keys in the target
+     * file are preserved. Propagates a {@link RuntimeException} on failure so 
the caller can log.
+     */
+    static void write(String key, String value) {
+        boolean local = livesInLocal(key);
+        try {
+            CommandLineHelper.createPropertyFile(local);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        String trimmed = trimToNull(value);
+        CommandLineHelper.loadProperties(props -> {
+            if (trimmed == null) {
+                props.remove(key);
+            } else {
+                props.setProperty(key, trimmed);
+            }
+            CommandLineHelper.storeProperties(props,
+                    new Printer.QuietPrinter(new Printer.SystemOutPrinter()), 
local);
+        }, local);
+    }
+
+    private static boolean livesInLocal(String key) {
+        if (!CommandLineHelper.hasLocalUserConfig()) {
+            return false;
+        }
+        boolean[] present = { false };
+        // Use getProperty (overridden by OrderedProperties) instead of 
containsKey
+        // (inherited from Hashtable and always empty in OrderedProperties).
+        CommandLineHelper.loadProperties(props -> present[0] = 
props.getProperty(key) != null, true);
+        return present[0];
+    }
+
+    private static String trimToNull(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
index ce3a4479c4a0..027afa416e1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
@@ -1,7 +1,7 @@
 # Camel TUI Theme Reference
 
 This document describes the CSS token system used by the Camel TUI.
-Every `.tcss` theme file must define all 27 tokens listed below.
+Every `.tcss` theme file must define all 28 tokens listed below.
 Missing tokens cause a startup validation error.
 
 ## File Format
@@ -52,13 +52,14 @@ $brand: #F69123;
 | `info` | fg | Informational accent (counts, prompts) |
 | `notice` | fg | Secondary accent, typically purple (e.g., TRACE log level) |
 
-### Content (3 tokens)
+### Content (4 tokens)
 
 | Token | Type | Purpose |
 |-------|------|---------|
 | `label` | fg | Field labels, section headers, key hints, table headers |
 | `change` | fg | Changed-value indicator in trace diffs |
 | `search-match` | fg+bg | Search/find match highlight |
+| `mnemonic` | fg+bold+underline | Keyboard shortcut mnemonic in tab headers 
and menus |
 
 ### Diagram (8 tokens)
 
@@ -66,12 +67,12 @@ $brand: #F69123;
 |-------|------|---------|
 | `diagram-border` | fg | Box-drawing borders in route diagrams |
 | `diagram-id` | fg | Route ID text |
-| `diagram-from` | fg | "from" EIP nodes |
-| `diagram-to` | fg | "to", "enrich", "marshal", "transform" nodes |
-| `diagram-choice` | fg | "choice", "when", "otherwise" nodes |
-| `diagram-action` | fg | "bean", "process", "log", "script" nodes |
-| `diagram-eip` | fg | Routing EIPs (split, aggregate, multicast, etc.) |
-| `diagram-default` | fg | Fallback for unknown EIP types |
+| `diagram-from` | fg | `from` EIP nodes |
+| `diagram-to` | fg | `to`, `enrich`, `marshal`, `transform` nodes |
+| `diagram-choice` | fg | `choice`, `when`, `otherwise` nodes |
+| `diagram-action` | fg | `bean`, `process`, `log`, `script` nodes |
+| `diagram-eip` | fg | Routing EIPs (`split`, `aggregate`, `multicast`, etc.) |
+| `diagram-default` | fg | Fallback for unspecified EIP types |
 
 ## Design Guidelines
 
@@ -96,3 +97,16 @@ When creating a new theme:
 |-------|------|-------------|
 | `dark` | `dark.tcss` | VS Code-inspired dark palette (default) |
 | `light` | `light.tcss` | GitHub-inspired light palette |
+| `dracula` | `dracula.tcss` | Dracula purple-and-pink-on-dark palette |
+| `nord` | `nord.tcss` | Nord arctic, bluish dark palette |
+| `solarized-dark` | `solarized-dark.tcss` | Solarized dark palette |
+| `solarized-light` | `solarized-light.tcss` | Solarized light palette |
+| `gruvbox-dark` | `gruvbox-dark.tcss` | Gruvbox retro-groove dark palette |
+| `catppuccin-mocha` | `catppuccin-mocha.tcss` | Catppuccin Mocha pastel dark 
palette |
+| `catppuccin-latte` | `catppuccin-latte.tcss` | Catppuccin Latte pastel light 
palette |
+| `tokyo-night` | `tokyo-night.tcss` | Tokyo Night neon-on-dark palette |
+| `rose-pine` | `rose-pine.tcss` | Rosé Pine muted dark palette |
+| `kanagawa` | `kanagawa.tcss` | Kanagawa Japanese-wave-inspired dark palette |
+| `everforest` | `everforest.tcss` | Everforest warm, green-forest dark 
palette |
+| `monochrome` | `monochrome.tcss` | Grayscale palette, no color, brightness 
only |
+| `crt` | `crt.tcss` | Retro green-phosphor terminal palette |
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopupTest.java
new file mode 100644
index 000000000000..95f2fdc9400f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopupTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Verifies the pre-filled folder precedence of {@link 
FolderInputPopup#open()}. The documented contract is that the
+ * most recently used folder wins, the configured default folder is used only 
when there is no remembered folder, and
+ * {@code user.dir} is the final fallback.
+ */
+@Isolated
+class FolderInputPopupTest {
+
+    private String originalHome;
+
+    @AfterEach
+    void tearDown() {
+        if (originalHome != null) {
+            CommandLineHelper.useHomeDir(originalHome);
+        }
+    }
+
+    private void useHome(Path dir) {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(dir.toString());
+    }
+
+    @Test
+    void lastFolderWinsOverDefaultFolder(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
+                
"camel.tui.defaultFolder=/opt/default\ncamel.tui.lastFolder=/home/user/recent\n");
+
+        FolderInputPopup popup = new FolderInputPopup(null);
+        popup.open();
+
+        assertEquals("/home/user/recent", popup.getInputText(),
+                "the most recently used folder must take precedence over the 
configured default");
+    }
+
+    @Test
+    void defaultFolderUsedWhenNoLastFolder(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
+                "camel.tui.defaultFolder=/opt/default\n");
+
+        FolderInputPopup popup = new FolderInputPopup(null);
+        popup.open();
+
+        assertEquals("/opt/default", popup.getInputText(),
+                "the configured default folder must be used when there is no 
remembered folder");
+    }
+
+    @Test
+    void fallsBackToUserDirWhenNothingConfigured(@TempDir Path tempDir) {
+        useHome(tempDir);
+
+        FolderInputPopup popup = new FolderInputPopup(null);
+        popup.open();
+
+        assertEquals(System.getProperty("user.dir"), popup.getInputText(),
+                "user.dir must be the final fallback when neither a last 
folder nor a default is set");
+    }
+}
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
new file mode 100644
index 000000000000..a61d9305461e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Renders {@link SettingsPopup} into a virtual buffer and asserts the dialog 
shows its title and all three setting
+ * rows, mirroring the other {@code *RenderTest} classes in this package.
+ */
+@Isolated
+class SettingsPopupRenderTest {
+
+    private String originalHome;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    @AfterEach
+    void tearDown() {
+        Theme.resetForTesting();
+        if (originalHome != null) {
+            CommandLineHelper.useHomeDir(originalHome);
+        }
+    }
+
+    private void useHome(Path dir) {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(dir.toString());
+    }
+
+    @Test
+    void rendersTitleAndAllThreeSettingRows(@TempDir Path tempDir) {
+        useHome(tempDir);
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(List.of(
+                new TabRegistry.TabEntry("🐪", "Overview", "overview", "1", 0, 
-1),
+                new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6, 
-1)));
+        popup.open();
+
+        Rect area = new Rect(0, 0, 80, 20);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        popup.render(frame, area);
+        String rendered = TuiTestHelper.bufferToString(buffer);
+
+        assertTrue(rendered.contains("Settings"), "the popup title should be 
shown");
+        assertTrue(rendered.contains("Theme"), "the Theme row should be 
shown");
+        assertTrue(rendered.contains("Starting Tab"), "the Starting Tab row 
should be shown");
+        assertTrue(rendered.contains("Default Folder"), "the Default Folder 
row should be shown");
+    }
+}
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
new file mode 100644
index 000000000000..7e30d6b60bb8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Drives {@link SettingsPopup} through key events to verify it seeds from 
persisted {@link TuiSettings}, cycles theme
+ * and starting-tab values, edits the folder field, persists on Enter 
(applying the theme live), and discards on Esc.
+ * Assertions check concrete persisted values, not merely non-null, so a 
change in save/apply logic will fail the test.
+ */
+@Isolated
+class SettingsPopupTest {
+
+    private String originalHome;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    @AfterEach
+    void tearDown() {
+        Theme.resetForTesting();
+        if (originalHome != null) {
+            CommandLineHelper.useHomeDir(originalHome);
+        }
+    }
+
+    private void useHome(Path dir) {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(dir.toString());
+    }
+
+    private static List<TabRegistry.TabEntry> tabs() {
+        return List.of(
+                new TabRegistry.TabEntry("🐪", "Overview", "overview", "1", 0, 
-1),
+                new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6, -1),
+                new TabRegistry.TabEntry("☕", "Beans", "beans", "B", 9, 0));
+    }
+
+    private static KeyEvent key(KeyCode code) {
+        return KeyEvent.ofKey(code, KeyModifiers.NONE);
+    }
+
+    @Test
+    void openSeedsCurrentValuesFromSettings(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
+                
"camel.tui.theme=light\ncamel.tui.startTab=Health\ncamel.tui.defaultFolder=/tmp/foo\n");
+
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+
+        assertTrue(popup.isVisible());
+        assertEquals("light", popup.selectedThemeId());
+        assertEquals("Health", popup.selectedStartTab());
+        assertEquals("/tmp/foo", popup.folderText());
+    }
+
+    @Test
+    void themeCyclerWrapsAroundValues(@TempDir Path tempDir) {
+        useHome(tempDir);
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+
+        assertEquals("dark", popup.selectedThemeId(), "with no config, theme 
defaults to the active mode (dark)");
+        popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertEquals("light", popup.selectedThemeId());
+
+        int themeCount = ThemeMode.ids().size();
+        for (int i = 0; i < themeCount - 1; i++) {
+            popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        }
+        assertEquals("dark", popup.selectedThemeId(), "cycling through every 
theme wraps back to the first value");
+    }
+
+    @Test
+    void startingTabCyclerWrapsAcrossAllTabs(@TempDir Path tempDir) {
+        useHome(tempDir);
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        assertEquals(1, popup.selectedRow());
+        assertEquals("Overview", popup.selectedStartTab());
+        popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertEquals("Health", popup.selectedStartTab());
+        popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertEquals("Beans", popup.selectedStartTab());
+        popup.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertEquals("Overview", popup.selectedStartTab(), "cycling wraps past 
the last tab");
+    }
+
+    @Test
+    void folderFieldAcceptsTypedText(@TempDir Path tempDir) {
+        useHome(tempDir);
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        assertEquals(2, popup.selectedRow());
+        popup.handleKeyEvent(KeyEvent.ofChar('/'));
+        popup.handleKeyEvent(KeyEvent.ofChar('a'));
+        assertEquals("/a", popup.folderText());
+    }
+
+    @Test
+    void folderFieldRejectsControlCharacters(@TempDir Path tempDir) {
+        useHome(tempDir);
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        assertEquals(2, popup.selectedRow());
+        popup.handleKeyEvent(KeyEvent.ofChar(0x01));
+        popup.handleKeyEvent(KeyEvent.ofChar(0x00));
+        popup.handleKeyEvent(KeyEvent.ofChar('x'));
+
+        assertEquals("x", popup.folderText());
+    }
+
+    @Test
+    void enterPersistsSelectionsAndAppliesThemeLive(@TempDir Path tempDir) 
throws Exception {
+        useHome(tempDir);
+        // Exercise the real persistence path (disk read/write) against the 
isolated home directory.
+        Theme.resetForTesting(false);
+
+        boolean[] cleared = { false };
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.setClearScreen(() -> cleared[0] = true);
+        popup.open();
+
+        popup.handleKeyEvent(KeyEvent.ofChar(' ')); // theme dark -> light
+        assertEquals("light", popup.selectedThemeId());
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        popup.handleKeyEvent(KeyEvent.ofChar(' ')); // starting tab Overview 
-> Health
+        assertEquals("Health", popup.selectedStartTab());
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        for (char c : "/tmp/p".toCharArray()) {
+            popup.handleKeyEvent(KeyEvent.ofChar(c));
+        }
+
+        popup.handleKeyEvent(key(KeyCode.ENTER));
+
+        assertFalse(popup.isVisible(), "Enter closes the popup");
+        assertTrue(cleared[0], "a theme change on save must trigger a screen 
clear");
+        assertEquals("light", Theme.mode(), "the theme must be applied live");
+
+        TuiSettings persisted = TuiSettings.load();
+        assertEquals("light", persisted.getThemeId());
+        assertEquals("Health", persisted.getStartTab());
+        assertEquals("/tmp/p", persisted.getDefaultFolder());
+    }
+
+    @Test
+    void escapeDiscardsInFormChanges(@TempDir Path tempDir) throws Exception {
+        useHome(tempDir);
+        Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG), 
"camel.tui.theme=dark\n");
+
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.open();
+        popup.handleKeyEvent(KeyEvent.ofChar(' ')); // cycle theme dark -> 
light, but do not save
+        popup.handleKeyEvent(key(KeyCode.ESCAPE));
+
+        assertFalse(popup.isVisible());
+        assertEquals("dark", TuiSettings.load().getThemeId(), "Esc must not 
persist the in-form change");
+        assertEquals("dark", Theme.mode(), "Esc must revert the live preview 
back to the theme active before open()");
+    }
+
+    @Test
+    void themeCyclingPreviewsLiveWithoutClosingThePopup(@TempDir Path tempDir) 
{
+        useHome(tempDir);
+        boolean[] cleared = { false };
+        SettingsPopup popup = new SettingsPopup();
+        popup.setTabEntries(tabs());
+        popup.setClearScreen(() -> cleared[0] = true);
+        popup.open();
+
+        assertEquals("dark", Theme.mode());
+        popup.handleKeyEvent(KeyEvent.ofChar(' ')); // theme dark -> light
+
+        assertTrue(popup.isVisible(), "cycling the theme row must not close 
the dialog");
+        assertEquals("light", Theme.mode(), "the previewed theme must apply 
immediately, without saving");
+        assertTrue(cleared[0], "the screen must refresh so the live preview is 
visible");
+    }
+}
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
new file mode 100644
index 000000000000..fd4fe0f4cff8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies {@link TuiSettings} load/mutate/save semantics against real 
(isolated) disk I/O: the full round-trip of all
+ * fields, blank-clears-the-key behavior, resilience to a missing file, and 
local-config precedence.
+ */
+@Isolated
+class TuiSettingsTest {
+
+    private String originalHome;
+
+    @AfterEach
+    void tearDown() {
+        if (originalHome != null) {
+            CommandLineHelper.useHomeDir(originalHome);
+        }
+    }
+
+    private void useHome(Path dir) {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(dir.toString());
+    }
+
+    @Test
+    void roundTripPreservesAllFields(@TempDir Path tempDir) {
+        useHome(tempDir);
+
+        TuiSettings settings = TuiSettings.load();
+        settings.setThemeId("light");
+        settings.setStartTab("Health");
+        settings.setDefaultFolder("/tmp/projects");
+        settings.save();
+
+        TuiSettings loaded = TuiSettings.load();
+        assertEquals("light", loaded.getThemeId());
+        assertEquals("Health", loaded.getStartTab());
+        assertEquals("/tmp/projects", loaded.getDefaultFolder());
+    }
+
+    @Test
+    void blankFieldRemovesItsKeyButKeepsOthers(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+
+        TuiSettings settings = TuiSettings.load();
+        settings.setThemeId("dark");
+        settings.setDefaultFolder("/tmp/x");
+        settings.save();
+
+        TuiSettings second = TuiSettings.load();
+        assertEquals("/tmp/x", second.getDefaultFolder());
+        second.setDefaultFolder("   ");
+        second.save();
+
+        TuiSettings third = TuiSettings.load();
+        assertNull(third.getDefaultFolder(), "a blank folder must clear its 
key");
+        assertEquals("dark", third.getThemeId(), "unrelated keys must remain");
+
+        String content = 
Files.readString(tempDir.resolve(CommandLineHelper.USER_CONFIG));
+        assertFalse(content.contains("camel.tui.defaultFolder"), "the cleared 
key must not be written back");
+    }
+
+    @Test
+    void missingFileYieldsNullFieldsWithoutThrowing(@TempDir Path tempDir) {
+        useHome(tempDir);
+
+        TuiSettings settings = TuiSettings.load();
+        assertNull(settings.getThemeId());
+        assertNull(settings.getStartTab());
+        assertNull(settings.getDefaultFolder());
+    }
+
+    @Test
+    void defaultFolderIsLoadedWhenPresentAlongsideLastFolder(@TempDir Path 
tempDir) throws Exception {
+        useHome(tempDir);
+        Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+
+        Files.writeString(globalFile,
+                
"camel.tui.defaultFolder=/projects/myapp\ncamel.tui.lastFolder=/tmp/other\n");
+
+        TuiSettings loaded = TuiSettings.load();
+        assertEquals("/projects/myapp", loaded.getDefaultFolder());
+
+        Files.writeString(globalFile, "camel.tui.lastFolder=/tmp/other\n");
+
+        TuiSettings withoutDefaultFolder = TuiSettings.load();
+        assertNull(withoutDefaultFolder.getDefaultFolder());
+    }
+
+    @Test
+    void localOverrideIsReadAndWrittenBackLocally(@TempDir Path tempDir) 
throws Exception {
+        // A project has deliberately pinned startTab in the local file.
+        // Editing it via the dialog should update the local file, not the 
global file.
+        useHome(tempDir);
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            Files.writeString(localFile, "camel.tui.startTab=Health\n");
+            assertTrue(CommandLineHelper.hasLocalUserConfig());
+
+            TuiSettings loaded = TuiSettings.load();
+            assertEquals("Health", loaded.getStartTab(), "load must read the 
local override");
+
+            loaded.setStartTab("Trace");
+            loaded.save();
+
+            String localContent = Files.readString(localFile);
+            assertTrue(localContent.contains("camel.tui.startTab=Trace"),
+                    "save must write a locally-overridden key back to the 
local file");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+
+    @Test
+    void newKeyGoesToGlobalEvenWhenLocalFileExists(@TempDir Path tempDir) 
throws Exception {
+        // A project has a local config for other CLI settings, but no 
camel.tui.defaultFolder.
+        // Saving defaultFolder must NOT dirty the project-local file.
+        useHome(tempDir);
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            Files.writeString(localFile, "camel.jbang.runtime=spring-boot\n");
+            assertTrue(CommandLineHelper.hasLocalUserConfig());
+
+            TuiSettings loaded = TuiSettings.load();
+            loaded.setDefaultFolder("/tmp/example");
+            loaded.save();
+
+            String localContent = Files.readString(localFile);
+            assertFalse(localContent.contains("camel.tui.defaultFolder"),
+                    "personal TUI state must not be written to the 
project-local file");
+            
assertTrue(Files.exists(tempDir.resolve(CommandLineHelper.USER_CONFIG)),
+                    "the global file must be created for the personal 
setting");
+            String globalContent = 
Files.readString(tempDir.resolve(CommandLineHelper.USER_CONFIG));
+            
assertTrue(globalContent.contains("camel.tui.defaultFolder=/tmp/example"),
+                    "personal TUI state must be written to the global file");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
new file mode 100644
index 000000000000..ab88f752bd6c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the per-key local/global routing in {@link TuiUserConfig}. These 
tests encode the two Codex adversarial
+ * review findings as regression guards:
+ * <ol>
+ * <li>A local config that only carries non-TUI keys must not shadow global 
TUI preferences.</li>
+ * <li>Writing a new TUI key must go to the global file, not into the 
project-local config.</li>
+ * </ol>
+ */
+@Isolated
+class TuiUserConfigTest {
+
+    private String originalHome;
+
+    @AfterEach
+    void tearDown() {
+        if (originalHome != null) {
+            CommandLineHelper.useHomeDir(originalHome);
+        }
+    }
+
+    private void useHome(Path dir) {
+        originalHome = CommandLineHelper.getHomeDir().toString();
+        CommandLineHelper.useHomeDir(dir.toString());
+    }
+
+    @Test
+    void readFallsBackToGlobalWhenKeyAbsentFromLocal(@TempDir Path tempDir) 
throws Exception {
+        useHome(tempDir);
+        Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(globalFile, "camel.tui.theme=light\n");
+
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            Files.writeString(localFile, "camel.jbang.runtime=spring-boot\n");
+
+            assertEquals("light", TuiUserConfig.read("camel.tui.theme"),
+                    "global theme must not be shadowed by an unrelated local 
config file");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+
+    @Test
+    void readPrefersLocalWhenKeyExistsLocally(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(globalFile, "camel.tui.startTab=Overview\n");
+
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            Files.writeString(localFile, "camel.tui.startTab=Health\n");
+
+            assertEquals("Health", TuiUserConfig.read("camel.tui.startTab"),
+                    "a local override must win over the global value");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+
+    @Test
+    void writeRoutesToGlobalForNewKey(@TempDir Path tempDir) throws Exception {
+        useHome(tempDir);
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            Files.writeString(localFile, "camel.jbang.runtime=spring-boot\n");
+
+            TuiUserConfig.write("camel.tui.theme", "dark");
+
+            String localContent = Files.readString(localFile);
+            assertFalse(localContent.contains("camel.tui.theme"),
+                    "writing a new TUI key must NOT dirty the project-local 
config");
+            Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+            assertTrue(Files.exists(globalFile), "the global file must be 
created");
+            String globalContent = Files.readString(globalFile);
+            assertTrue(globalContent.contains("camel.tui.theme=dark"),
+                    "the new key must land in the global file");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+
+    @Test
+    void writeUpdatesLocalForExistingLocalKey(@TempDir Path tempDir) throws 
Exception {
+        useHome(tempDir);
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            Files.writeString(localFile, "camel.tui.startTab=Health\n");
+
+            TuiUserConfig.write("camel.tui.startTab", "Trace");
+
+            String localContent = Files.readString(localFile);
+            assertTrue(localContent.contains("camel.tui.startTab=Trace"),
+                    "an existing local override must be updated in the local 
file");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+
+    @Test
+    void writeUpdatesLocalForExistingLocalKeyWithEmptyValue(@TempDir Path 
tempDir) throws Exception {
+        useHome(tempDir);
+        Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(globalFile, "camel.tui.startTab=Overview\n");
+
+        Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+        try {
+            // An explicitly-set but empty-valued local key must still be 
recognized as a local override
+            // (an empty property line loads as "", not null).
+            Files.writeString(localFile, "camel.tui.startTab=\n");
+
+            TuiUserConfig.write("camel.tui.startTab", "Trace");
+
+            String localContent = Files.readString(localFile);
+            assertTrue(localContent.contains("camel.tui.startTab=Trace"),
+                    "an existing empty-valued local override must be updated 
in the local file, not the global one");
+            String globalContent = Files.readString(globalFile);
+            assertFalse(globalContent.contains("camel.tui.startTab=Trace"),
+                    "the write must not leak into the global file");
+        } finally {
+            Files.deleteIfExists(localFile);
+        }
+    }
+
+    @Test
+    void readReturnsNullWhenKeyAbsentFromBothFiles(@TempDir Path tempDir) {
+        useHome(tempDir);
+        assertNull(TuiUserConfig.read("camel.tui.theme"));
+    }
+
+    @Test
+    void writeBlankRemovesKey(@TempDir Path tempDir) throws Exception {
+        useHome(tempDir);
+        Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+        Files.writeString(globalFile, "camel.tui.theme=dark\n");
+
+        TuiUserConfig.write("camel.tui.theme", "   ");
+
+        String content = Files.readString(globalFile);
+        assertFalse(content.contains("camel.tui.theme"), "a blank value must 
remove the key");
+    }
+}
diff --git 
a/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
 
b/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
index d24d1766cb4e..7fe9312b3ff8 100644
--- 
a/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
+++ 
b/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
@@ -492,7 +492,7 @@ public class CliLocalProcessService implements CliService {
     }
 
     private Path getUserPropertiesFile() {
-        return Path.of(System.getProperty("user.home"), 
".camel-jbang-user.properties");
+        return Path.of(System.getProperty("user.home"), 
".camel-cli.properties");
     }
 
     private void backupUserFiles() {


Reply via email to