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
commit 915e4829f647f7b227b2c496ac696c88e5c1b161 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Sep 8 16:29:24 2026 +0200 CAMEL-24656: camel-jbang TUI - add an AI Tools row to the F2 Settings popup Cycles auto / core / full for the tool set the AI panel sends to the model, persisted as camel.tui.ai.tools like the /tools slash command. The panel re-reads the setting before each question so a change applies without a restart. Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../modules/ROOT/pages/camel-jbang-tui.adoc | 7 ++-- .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 6 ++- .../dsl/jbang/core/commands/tui/SettingsPopup.java | 39 ++++++++++++++++-- .../jbang/core/commands/tui/SettingsPopupTest.java | 47 ++++++++++++++++++++-- 4 files changed, 89 insertions(+), 10 deletions(-) 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 49380e035949..b5716f7eceb6 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc @@ -557,7 +557,8 @@ 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.selectTab`, `camel.tui.confirmActions`, `camel.tui.defaultFolder`, `camel.tui.panelPosition`, `camel.tui.panelSpace`, -`camel.tui.shell.history`, `camel.tui.ai.promptHistory`) in the Camel CLI configuration file. Each key is read from and +`camel.tui.shell.history`, `camel.tui.ai.provider`, `camel.tui.ai.model`, `camel.tui.ai.url`, +`camel.tui.ai.tools`, `camel.tui.ai.promptHistory`) 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 @@ -822,8 +823,8 @@ tools (state, tables, logs, errors, diagrams, topology, processor details, catal spans, route control, sending messages, source files, navigation, log level and filters) to Ollama and to any provider on `localhost`, which roughly halves the prompt. Hosted providers get every tool, including the drawing, animation and automation tools. Use `/tools full` in the panel to send -all tools to a local model too, `/tools core` to trim the set for a hosted one, or set -`camel.tui.ai.tools` in `.camel-cli.properties`. Ollama requests also ask the server to keep the +all tools to a local model too, `/tools core` to trim the set for a hosted one, pick *AI Tools* in +*F2 -> Settings*, or set `camel.tui.ai.tools` in `.camel-cli.properties`. Ollama requests also ask the server to keep the model loaded for 30 minutes and use a 32k context window (`OLLAMA_CONTEXT_LENGTH` overrides it), so follow-up questions reuse the cached prompt instead of reloading the model. diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java index 1aa3722125bf..0ec870f14ef1 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java @@ -964,7 +964,11 @@ class AiPanel { thinkingStartTime = System.currentTimeMillis(); thinking.set(true); - // rebuild tools in case mcpFacade was wired after init + // re-read the tool mode so a change made in F2 -> Settings applies to the next question, and rebuild the + // tools in case mcpFacade was wired after init + if (!testingClientInjected) { + toolMode = normalizeToolMode(TuiSettings.load().getAiTools()); + } tools = buildTuiToolDefinitions(); String systemPrompt = buildSystemPrompt(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java index 67e0d02e3505..aea9a61533c3 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java @@ -61,13 +61,16 @@ class SettingsPopup { private static final int ROW_AI_PROVIDER = 13; private static final int ROW_AI_MODEL = 14; private static final int ROW_AI_URL = 15; - private static final int ROW_AI_PROMPT_HISTORY = 16; - private static final int ROW_COUNT = 17; + private static final int ROW_AI_TOOLS = 16; + private static final int ROW_AI_PROMPT_HISTORY = 17; + private static final int ROW_COUNT = 18; private static final String[] LOG_PIN_OPTIONS = { "off", "25", "50", "75" }; private static final String[] RATE_PER_OPTIONS = { "seconds", "minutes" }; private static final String[] PANEL_POSITION_OPTIONS = { "bottom", "top" }; private static final String[] PANEL_SPACE_OPTIONS = { "move", "overlay" }; + private static final String[] AI_TOOLS_OPTIONS + = { AiPanel.TOOL_MODE_AUTO, AiPanel.TOOL_MODE_CORE, AiPanel.TOOL_MODE_FULL }; private static final List<String> AI_PROVIDERS = buildAiProviderList(); private static List<String> buildAiProviderList() { @@ -93,6 +96,7 @@ class SettingsPopup { private int confirmActionsIndex; private int validateOnSaveIndex; private int aiProviderIndex; + private int aiToolsIndex; private TextInputState folderInput; private TextInputState proxyHostInput; private TextInputState proxyPortInput; @@ -181,6 +185,9 @@ class SettingsPopup { aiProviderIndex = providerIdx >= 0 ? providerIdx : AI_PROVIDERS.indexOf("auto"); aiModelInput = new TextInputState(settings.getAiModel() != null ? settings.getAiModel() : ""); aiUrlInput = new TextInputState(settings.getAiUrl() != null ? settings.getAiUrl() : ""); + String currentTools = AiPanel.normalizeToolMode(settings.getAiTools()); + int toolsIdx = List.of(AI_TOOLS_OPTIONS).indexOf(currentTools != null ? currentTools : AiPanel.TOOL_MODE_AUTO); + aiToolsIndex = Math.max(0, toolsIdx); aiPromptHistoryInput = new TextInputState( settings.getAiPromptHistory() != null ? settings.getAiPromptHistory() : ""); selectedRow = ROW_THEME; @@ -323,6 +330,14 @@ class SettingsPopup { handleTextInput(ke, aiUrlInput); return true; } + if (selectedRow == ROW_AI_TOOLS) { + if (ke.isChar(' ') || ke.isRight()) { + aiToolsIndex = (aiToolsIndex + 1) % AI_TOOLS_OPTIONS.length; + } else if (ke.isLeft()) { + aiToolsIndex = (aiToolsIndex - 1 + AI_TOOLS_OPTIONS.length) % AI_TOOLS_OPTIONS.length; + } + return true; + } if (selectedRow == ROW_AI_PROMPT_HISTORY) { handleTextInput(ke, aiPromptHistoryInput); return true; @@ -368,6 +383,8 @@ class SettingsPopup { settings.setAiProvider(AI_PROVIDERS.get(aiProviderIndex)); settings.setAiModel(stripControlChars(aiModelInput.text().trim())); settings.setAiUrl(stripControlChars(aiUrlInput.text().trim())); + String aiToolsValue = AI_TOOLS_OPTIONS[aiToolsIndex]; + settings.setAiTools(AiPanel.TOOL_MODE_AUTO.equals(aiToolsValue) ? null : aiToolsValue); settings.setAiPromptHistory(stripControlChars(aiPromptHistoryInput.text().trim())); settings.save(); if (Theme.mode().equals(selectedThemeId)) { @@ -495,6 +512,10 @@ class SettingsPopup { renderTextInput(frame, innerX + labelW, rowY, fieldW, aiUrlInput, selectedRow == ROW_AI_URL, "(auto)"); rowY++; + renderLabel(frame, innerX, rowY, labelW, "AI Tools:", selectedRow == ROW_AI_TOOLS); + renderValue(frame, innerX + labelW, rowY, fieldW, aiToolsLabel(), selectedRow == ROW_AI_TOOLS); + rowY++; + renderLabel(frame, innerX, rowY, labelW, "AI History:", selectedRow == ROW_AI_PROMPT_HISTORY); renderTextInput(frame, innerX + labelW, rowY, fieldW, aiPromptHistoryInput, selectedRow == ROW_AI_PROMPT_HISTORY, "(100)"); @@ -505,7 +526,7 @@ class SettingsPopup { || selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER || selectedRow == ROW_PANEL_POSITION || selectedRow == ROW_PANEL_SPACE || selectedRow == ROW_CONFIRM_ACTIONS || selectedRow == ROW_VALIDATE_ON_SAVE - || selectedRow == ROW_AI_PROVIDER) { + || selectedRow == ROW_AI_PROVIDER || selectedRow == ROW_AI_TOOLS) { hint(spans, "Space", "cycle"); } hint(spans, "Enter", "save"); @@ -637,6 +658,18 @@ class SettingsPopup { return AI_PROVIDERS.get(aiProviderIndex); } + String selectedAiTools() { + return AI_TOOLS_OPTIONS[aiToolsIndex]; + } + + private String aiToolsLabel() { + return switch (AI_TOOLS_OPTIONS[aiToolsIndex]) { + case AiPanel.TOOL_MODE_CORE -> "core (troubleshooting only)"; + case AiPanel.TOOL_MODE_FULL -> "full (all tools)"; + default -> "auto (core if local model)"; + }; + } + String aiModelText() { return aiModelInput != null ? aiModelInput.text() : ""; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java index efba01b08d02..7dc071e4d1c4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java @@ -32,6 +32,7 @@ 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; /** @@ -216,6 +217,46 @@ class SettingsPopupTest { assertEquals("https://example.test", persisted.getAiUrl()); } + @Test + void aiToolsRowCyclesModesAndPersistsNonDefault(@TempDir Path tempDir) { + useHome(tempDir); + SettingsPopup popup = new SettingsPopup(); + popup.setTabEntries(tabs()); + popup.open(); + + // navigate to AI Tools (row 16) + for (int i = 0; i < 16; i++) { + popup.handleKeyEvent(key(KeyCode.DOWN)); + } + assertEquals(16, popup.selectedRow()); + assertEquals("auto", popup.selectedAiTools()); + + popup.handleKeyEvent(KeyEvent.ofChar(' ')); + assertEquals("core", popup.selectedAiTools()); + popup.handleKeyEvent(key(KeyCode.RIGHT)); + assertEquals("full", popup.selectedAiTools()); + popup.handleKeyEvent(key(KeyCode.RIGHT)); + assertEquals("auto", popup.selectedAiTools()); + popup.handleKeyEvent(key(KeyCode.LEFT)); + assertEquals("full", popup.selectedAiTools()); + + popup.handleKeyEvent(key(KeyCode.ENTER)); + assertEquals("full", TuiSettings.load().getAiTools()); + + // auto is the default and is not written to the settings file + SettingsPopup reopened = new SettingsPopup(); + reopened.setTabEntries(tabs()); + reopened.open(); + assertEquals("full", reopened.selectedAiTools()); + for (int i = 0; i < 16; i++) { + reopened.handleKeyEvent(key(KeyCode.DOWN)); + } + reopened.handleKeyEvent(key(KeyCode.RIGHT)); + assertEquals("auto", reopened.selectedAiTools()); + reopened.handleKeyEvent(key(KeyCode.ENTER)); + assertNull(TuiSettings.load().getAiTools()); + } + @Test void historyFieldsPersistValues(@TempDir Path tempDir) { useHome(tempDir); @@ -233,11 +274,11 @@ class SettingsPopupTest { } assertEquals("50", popup.shellHistoryText()); - // navigate to AI Prompt History (row 14) - for (int i = 0; i < 4; i++) { + // navigate to AI Prompt History (row 17) + for (int i = 0; i < 5; i++) { popup.handleKeyEvent(key(KeyCode.DOWN)); } - assertEquals(16, popup.selectedRow()); + assertEquals(17, popup.selectedRow()); for (char c : "200".toCharArray()) { popup.handleKeyEvent(KeyEvent.ofChar(c)); }
