You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "ammachado (via GitHub)" <gi...@apache.org> on 2026/07/10 20:50:53 UTC
[PR] [CAMEL-23978] Add AI panel slash commands to Camel TUI [camel]
ammachado opened a new pull request, #24594:
URL: https://github.com/apache/camel/pull/24594
# Description
Adds local slash commands to the Camel TUI AI panel so operators can control the panel and invoke Camel CLI actions without sending prompts to the configured AI provider.
**How it works**
- Input starting with `/` is routed through `AiSlashCommandRegistry` instead of the LLM chat path.
- Built-in panel commands: `/help`, `/provider`, `/model`, `/clear`, `/close`, `/quit` (with `/exit`, `/q`, `/x` aliases).
- CLI bridge commands: `/run`, `/infra`, and `/send` execute `camel run`, `camel infra`, and `camel cmd send` via a TUI-safe `AiCliCommandExecutor` that preserves the dashboard process, captures output, and supports cancellation.
- `/provider` and `Ctrl+P` open a new `AiProviderSwitchPopup` for switching AI providers.
- Live command hints and dimmed parameter placeholders appear while typing slash commands; `/help` renders aligned descriptions in the panel.
- Provider/model changes are deferred while the AI panel is busy; unknown commands suggest `/help`.
- `/quit` and aliases exit the TUI through `CamelMonitor`.
**Documentation**
- User manual updated with the supported slash commands and their behavior.
**Tests**
- `AiSlashCommandRegistryTest`, `AiCliCommandExecutorTest`, `AiPanelTest`, `AiProviderSwitchPopupTest`, and updated `CamelMonitorTest` / `AiCliCommandExecutorTest` coverage.
# Target
- [x] I checked that the commit is targeting the correct branch (Camel 4 uses the `main` branch)
# Tracking
- [x] If this is a large change, bug fix, or code improvement, I checked there is a [JIRA issue](https://issues.apache.org/jira/browse/CAMEL) filed for the change (usually before you start working on it).
https://issues.apache.org/jira/browse/CAMEL-23978
# Apache Camel coding standards and style
- [x] I checked that each commit in the pull request has a meaningful subject line and body.
- [ ] I have run `mvn clean install -DskipTests` locally from root folder and I have committed all auto-generated changes.
# AI-assisted contributions
- [x] If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., `Co-authored-by` trailers) and the PR description identifies the AI tool used.
_AI-generated on behalf of ammachado using Cursor and Codex. Commits include `Co-authored-by: Cursor <cu...@cursor.com>` and `Co-authored-by: Codex <no...@openai.com>` trailers where applicable._
Made with [Cursor](https://cursor.com)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4948173011
okay was about to, let us know when its ready next time
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3564913656
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -187,22 +234,29 @@ void open() {
void close() {
visible = false;
+ providerSwitchPopup.close();
}
void destroy() {
close();
- Thread t = agentThread;
- if (t != null) {
- t.interrupt();
- }
+ stopAgentThread();
}
private void initClient() {
try {
- client = LlmClient.create()
+ LlmClient created = LlmClient.create()
.withTemperature(0.3)
.withTimeout(120)
- .withMaxTokens(4096);
+ .withMaxTokens(4096)
+ .withModel("llama3.2");
Review Comment:
Fixed in 0db17789f02e — removed the hard-coded `llama3.2` call from `AiPanel.initClient()`. A blank configured model now leaves model selection to the selected provider, as it did before the provider-switch change.
_AI-generated by Codex on behalf of ammachado._
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java:
##########
@@ -1126,11 +1215,41 @@ private String sendAnthropicStreamingRequest(String requestUrl, JsonObject body,
// ---- Endpoint detection helpers ----
+ // Order matters: /api/tags is checked before /v1/models, matching the original priority.
+ private static final List<Map.Entry<String, ApiType>> EXPLICIT_URL_HEALTH_CHECK_SUFFIXES = List.of(
Review Comment:
Verified against the pre-change implementation: `tryExplicitUrl()` previously delegated to `isEndpointReachable(url)`, which already checked `url + "/api/tags"`, then `url + "/v1/models"`, then the bare URL. The current implementation preserves that request order and count; its only added behavior is inferring `apiType` from a successful known endpoint when no API type was configured. No code change is needed for this comment.
_AI-generated by Codex on behalf of ammachado._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3564655968
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -83,21 +94,37 @@ record LogEntry(String timestamp, LogLevel level, String message, String detail)
private final StringBuilder inputBuffer = new StringBuilder();
private int cursorPos;
- // Conversation display
+ // Conversation display. CopyOnWriteArrayList because entries are appended from the agent thread and the
+ // CLI-command-completion callback while the render thread iterates the list concurrently.
private final List<ConversationEntry> conversation = new CopyOnWriteArrayList<>();
private int scrollOffset;
// LLM state
- private LlmClient client;
+ private volatile LlmClient client;
private List<LlmClient.Message> messages;
private List<LlmClient.ToolDef> tools;
private AskTools askTools;
private final AtomicBoolean thinking = new AtomicBoolean();
private volatile Thread agentThread;
private String initError;
private long thinkingStartTime;
+ private String thinkingVerb;
private volatile int sessionTotalTokens;
Review Comment:
Fixed in ae38606cb33372c942a01f763da8397f7252668b — `thinkingVerb` is now `volatile` so its value is visible across the UI thread and the CLI completion callback thread.
_Claude Code on behalf of ammachado_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3564656442
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java:
##########
@@ -287,6 +287,95 @@ public ChatResponse chatWithTools(String systemPrompt, List<Message> messages, L
};
}
+ // -- Model listing --
+
+ /**
+ * Lists model identifiers available from the configured provider: Ollama's {@code /api/tags}, or the
+ * OpenAI-compatible/Anthropic {@code /v1/models}. Vertex AI has no equivalent listing endpoint (models come from
+ * the Vertex AI model garden, not the Anthropic API surface), so it always returns an empty list. Best-effort: any
+ * failure (unreachable endpoint, non-200 response, unexpected JSON shape) yields an empty list rather than
+ * throwing, since this only feeds the informational {@code /model} listing in the AI panel.
+ */
+ public List<String> listModels() {
+ if (apiType == null || url == null || url.isBlank()) {
Review Comment:
Documented in the commit message of ae38606cb33372c942a01f763da8397f7252668b: `tryExplicitUrl()` now probes `/api/tags` and `/v1/models` before the bare URL and sets `apiType` as a side effect when a probe succeeds. Custom proxy/gateway URLs that do not serve those sub-paths still fall through to the bare-URL reachability check, at the cost of two extra probe round-trips. No behavioral revert; keeping the auto-detection.
_Claude Code on behalf of ammachado_
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java:
##########
@@ -122,7 +122,8 @@ public Integer doCall() throws Exception {
if (!client.detectEndpoint()) {
printer().printErr("LLM service is not reachable.");
- printer().printErr("Options: --url=<endpoint>, --api-type=anthropic, or start Ollama with: camel infra run ollama");
+ printer()
Review Comment:
Reverted in ae38606cb33372c942a01f763da8397f7252668b — the `printErr` line is back to a single line, matching `main`. Confirmed `formatter:format` does not re-wrap it (the line is within the 128-char limit).
_Claude Code on behalf of ammachado_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4947934369
@gnodet Fixed in ae38606cb33372c942a01f763da8397f7252668b — `AiSlashCommandRegistryTest` now imports `java.util.concurrent.CompletableFuture` and uses the simple name in `NoopSlashContext` (both occurrences), per the no-FQCN coding convention.
_Claude Code on behalf of ammachado_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3564893489
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java:
##########
@@ -1126,11 +1215,41 @@ private String sendAnthropicStreamingRequest(String requestUrl, JsonObject body,
// ---- Endpoint detection helpers ----
+ // Order matters: /api/tags is checked before /v1/models, matching the original priority.
+ private static final List<Map.Entry<String, ApiType>> EXPLICIT_URL_HEALTH_CHECK_SUFFIXES = List.of(
Review Comment:
The previous `tryExplicitUrl()` made a single `isEndpointReachable(url)` call. This now probes `url + "/api/tags"` and `url + "/v1/models"` first, adding 1–2 extra HTTP requests during endpoint detection. If a non-Ollama service happens to respond at `/api/tags`, the API type could be mis-detected. Worth a quick note in the PR description or a comment here explaining the detection priority.
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -187,22 +234,29 @@ void open() {
void close() {
visible = false;
+ providerSwitchPopup.close();
}
void destroy() {
close();
- Thread t = agentThread;
- if (t != null) {
- t.interrupt();
- }
+ stopAgentThread();
}
private void initClient() {
try {
- client = LlmClient.create()
+ LlmClient created = LlmClient.create()
.withTemperature(0.3)
.withTimeout(120)
- .withMaxTokens(4096);
+ .withMaxTokens(4096)
+ .withModel("llama3.2");
Review Comment:
The default model `llama3.2` is set here before `applyChoice()` runs. If the user configures `camel.tui.ai.provider=anthropic` (or `openai`) in settings but leaves `camel.tui.ai.model` blank, `applyChoice()` skips the model override (because `model` is null/blank) and `llama3.2` is sent to the Anthropic/OpenAI API, which would reject it.
Before this PR, `initClient()` did not set a default model at all.
Consider either:
- Removing the default and letting each provider use its own default, or
- Setting the default conditionally based on the detected/configured provider.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3565015591
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -323,14 +443,200 @@ boolean handleKeyEvent(KeyEvent ke) {
return true;
}
- private void submitQuestion() {
- String question = inputBuffer.toString().trim();
+ /**
+ * Completes the slash command name at the cursor. With multiple matches, TAB first fills in the longest common
+ * prefix; once no further prefix can be added it cycles forward through the matches (wrapping so every match is
+ * reachable with TAB alone). A single match is completed fully and a trailing space is appended. Shift+TAB cycles
+ * backward. TAB is a no-op unless the buffer is a partial command name (starts with {@code /}, no arguments yet).
+ */
+ private void handleTabCompletion(boolean backward) {
+ String text = inputBuffer.toString();
+ boolean continuing = text.equals(completionSnapshot) && completionMatches != null && completionMatches.size() > 1;
+ if (continuing) {
+ int size = completionMatches.size();
+ if (completionCycleIndex < 0) {
+ completionCycleIndex = backward ? size - 1 : 0;
+ } else {
+ completionCycleIndex = backward
+ ? (completionCycleIndex - 1 + size) % size
+ : (completionCycleIndex + 1) % size;
+ }
+ applyCompletionToken(completionMatches.get(completionCycleIndex), false);
+ return;
+ }
+
+ List<String> names = slashCommands.completionsFor(text).stream()
+ .map(AiSlashCommandRegistry.Descriptor::name)
+ .toList();
+ if (names.isEmpty()) {
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ if (names.size() == 1) {
+ applyCompletionToken(names.get(0), true);
+ // A single completion ends with a trailing space, so there is nothing left to cycle.
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ String currentToken = text.substring(1);
+ String prefix = longestCommonPrefix(names);
+ completionMatches = names;
+ if (prefix.length() > currentToken.length()) {
+ applyCompletionToken(prefix, false);
+ completionCycleIndex = -1;
+ } else {
+ completionCycleIndex = backward ? names.size() - 1 : 0;
+ applyCompletionToken(names.get(completionCycleIndex), false);
+ }
+ }
+
+ private void applyCompletionToken(String token, boolean trailingSpace) {
+ inputBuffer.setLength(0);
+ inputBuffer.append('/').append(token);
+ if (trailingSpace) {
+ inputBuffer.append(' ');
+ }
+ cursorPos = inputBuffer.length();
+ completionSnapshot = inputBuffer.toString();
+ }
+
+ private static String longestCommonPrefix(List<String> values) {
+ String prefix = values.get(0);
+ for (int i = 1; i < values.size() && !prefix.isEmpty(); i++) {
+ String value = values.get(i);
+ int max = Math.min(prefix.length(), value.length());
+ int j = 0;
+ while (j < max && prefix.charAt(j) == value.charAt(j)) {
+ j++;
+ }
+ prefix = prefix.substring(0, j);
+ }
+ return prefix;
+ }
+
+ private void submitInput() {
+ String input = inputBuffer.toString().trim();
inputBuffer.setLength(0);
cursorPos = 0;
scrollOffset = 0;
+ if (input.startsWith("/")) {
+ executeSlashCommand(input);
+ } else {
+ submitQuestion(input);
+ }
+ }
+
+ private void executeSlashCommand(String input) {
+ Optional<AiSlashCommandRegistry.ParsedCommand> parsed = slashCommands.parse(input);
+ if (parsed.isPresent() && thinking.get()) {
+ String name = parsed.get().descriptor().name();
+ if ("provider".equals(name) || "model".equals(name)) {
+ conversation.add(new ConversationEntry(
+ AiRole.SYSTEM,
+ "Wait for the current operation to finish before changing provider or model."));
+ return;
+ }
+ }
+
+ AiSlashCommandRegistry.CommandResult result = slashCommands.execute(input, slashCommandContext);
+ if (result.cliRequest() != null) {
+ AiCliCommandExecutor.Request request = result.cliRequest();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "Running " + request.displayText()));
+ // Runs in the background without entering the panel's "thinking" state, so the user can keep typing
+ // and asking questions while the command runs. Esc still cancels it via interruptBusyOperation().
+ CompletableFuture<AiCliCommandExecutor.Result> future = slashCommandContext.executeCli(request);
+ activeCliCommand = future;
+ future.whenComplete(this::handleCliCompletion);
+ return;
+ }
+ if (result.text() != null && !result.text().isBlank()) {
+ conversation.add(new ConversationEntry(result.role(), result.text()));
+ }
+ }
+
+ private void handleCliCompletion(AiCliCommandExecutor.Result cliResult, Throwable error) {
+ CompletableFuture<AiCliCommandExecutor.Result> future = activeCliCommand;
+ synchronized (this) {
+ if (future == null || activeCliCommand != future) {
+ return;
+ }
+ activeCliCommand = null;
+ }
+ thinking.set(false);
+ thinkingVerb = null;
+
+ if (error != null) {
+ String displayText = cliResult != null ? cliResult.displayText() : "command";
+ conversation.add(new ConversationEntry(
+ AiRole.ERROR, "Failed to run " + displayText + ": " + error.getMessage()));
+ scrollOffset = 0;
+ return;
+ }
+
+ if (cliResult.exitCode() == 0) {
+ conversation.add(new ConversationEntry(
+ AiRole.SYSTEM,
+ cliResult.displayText() + " completed in " + cliResult.elapsedMs() + " ms\n\n" + cliResult.output()));
+ } else {
+ conversation.add(new ConversationEntry(
+ AiRole.ERROR,
+ cliResult.displayText() + " exit code " + cliResult.exitCode() + "\n\n" + cliResult.output()));
+ }
+ scrollOffset = 0;
+ }
+
+ private void interruptBusyOperation() {
+ if (activeCliCommand != null) {
+ activeCliCommand = null;
+ slashCommandContext.cancelCli();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "(command cancelled)"));
+ thinking.set(false);
+ thinkingVerb = null;
+ } else {
+ stopAgentThread();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "(cancelled)"));
+ }
+ }
+
+ private void stopAgentThread() {
+ Thread t = agentThread;
+ if (t == null) {
+ return;
+ }
+ if (t != Thread.currentThread()) {
+ t.interrupt();
+ // Unlike AiCliCommandExecutor.cancel(), this join is not offloaded to a watcher thread: it runs
+ // on the caller, which is either the interactive UI thread (Esc/Ctrl+C via interruptBusyOperation)
+ // or destroy() during shutdown. If the LLM HTTP call in runAgentLoop() ignores the interrupt (e.g.
+ // a client/library that doesn't check the interrupt flag mid-request), this blocks that caller for
+ // up to 30s. Left blocking deliberately for now because AiPanelTest asserts synchronous
+ // cancellation semantics (isAgentThreadRunningForTesting() is false immediately after Esc); making
+ // this async would need those tests reworked to poll instead.
+ try {
+ t.join(30_000);
Review Comment:
This `t.join(30_000)` runs on the caller's thread (typically the TUI event/render thread). If an LLM HTTP client ignores the interrupt flag mid-request, this freezes the entire UI for up to 30 seconds. The code comment explains the test-driven rationale well, but it's still a user-facing risk.
`AiCliCommandExecutor.cancel()` already solves this same problem by offloading to a watcher thread — consider applying the same pattern here and reworking the test assertions to poll with Awaitility (which the project already uses).
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -323,14 +443,200 @@ boolean handleKeyEvent(KeyEvent ke) {
return true;
}
- private void submitQuestion() {
- String question = inputBuffer.toString().trim();
+ /**
+ * Completes the slash command name at the cursor. With multiple matches, TAB first fills in the longest common
+ * prefix; once no further prefix can be added it cycles forward through the matches (wrapping so every match is
+ * reachable with TAB alone). A single match is completed fully and a trailing space is appended. Shift+TAB cycles
+ * backward. TAB is a no-op unless the buffer is a partial command name (starts with {@code /}, no arguments yet).
+ */
+ private void handleTabCompletion(boolean backward) {
+ String text = inputBuffer.toString();
+ boolean continuing = text.equals(completionSnapshot) && completionMatches != null && completionMatches.size() > 1;
+ if (continuing) {
+ int size = completionMatches.size();
+ if (completionCycleIndex < 0) {
+ completionCycleIndex = backward ? size - 1 : 0;
+ } else {
+ completionCycleIndex = backward
+ ? (completionCycleIndex - 1 + size) % size
+ : (completionCycleIndex + 1) % size;
+ }
+ applyCompletionToken(completionMatches.get(completionCycleIndex), false);
+ return;
+ }
+
+ List<String> names = slashCommands.completionsFor(text).stream()
+ .map(AiSlashCommandRegistry.Descriptor::name)
+ .toList();
+ if (names.isEmpty()) {
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ if (names.size() == 1) {
+ applyCompletionToken(names.get(0), true);
+ // A single completion ends with a trailing space, so there is nothing left to cycle.
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ String currentToken = text.substring(1);
+ String prefix = longestCommonPrefix(names);
+ completionMatches = names;
+ if (prefix.length() > currentToken.length()) {
+ applyCompletionToken(prefix, false);
+ completionCycleIndex = -1;
+ } else {
+ completionCycleIndex = backward ? names.size() - 1 : 0;
+ applyCompletionToken(names.get(completionCycleIndex), false);
+ }
+ }
+
+ private void applyCompletionToken(String token, boolean trailingSpace) {
+ inputBuffer.setLength(0);
+ inputBuffer.append('/').append(token);
+ if (trailingSpace) {
+ inputBuffer.append(' ');
+ }
+ cursorPos = inputBuffer.length();
+ completionSnapshot = inputBuffer.toString();
+ }
+
+ private static String longestCommonPrefix(List<String> values) {
+ String prefix = values.get(0);
+ for (int i = 1; i < values.size() && !prefix.isEmpty(); i++) {
+ String value = values.get(i);
+ int max = Math.min(prefix.length(), value.length());
+ int j = 0;
+ while (j < max && prefix.charAt(j) == value.charAt(j)) {
+ j++;
+ }
+ prefix = prefix.substring(0, j);
+ }
+ return prefix;
+ }
+
+ private void submitInput() {
+ String input = inputBuffer.toString().trim();
inputBuffer.setLength(0);
cursorPos = 0;
scrollOffset = 0;
+ if (input.startsWith("/")) {
+ executeSlashCommand(input);
+ } else {
+ submitQuestion(input);
+ }
+ }
+
+ private void executeSlashCommand(String input) {
+ Optional<AiSlashCommandRegistry.ParsedCommand> parsed = slashCommands.parse(input);
+ if (parsed.isPresent() && thinking.get()) {
+ String name = parsed.get().descriptor().name();
+ if ("provider".equals(name) || "model".equals(name)) {
+ conversation.add(new ConversationEntry(
+ AiRole.SYSTEM,
+ "Wait for the current operation to finish before changing provider or model."));
+ return;
+ }
+ }
+
+ AiSlashCommandRegistry.CommandResult result = slashCommands.execute(input, slashCommandContext);
+ if (result.cliRequest() != null) {
+ AiCliCommandExecutor.Request request = result.cliRequest();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "Running " + request.displayText()));
+ // Runs in the background without entering the panel's "thinking" state, so the user can keep typing
+ // and asking questions while the command runs. Esc still cancels it via interruptBusyOperation().
+ CompletableFuture<AiCliCommandExecutor.Result> future = slashCommandContext.executeCli(request);
+ activeCliCommand = future;
+ future.whenComplete(this::handleCliCompletion);
+ return;
+ }
+ if (result.text() != null && !result.text().isBlank()) {
+ conversation.add(new ConversationEntry(result.role(), result.text()));
+ }
+ }
+
+ private void handleCliCompletion(AiCliCommandExecutor.Result cliResult, Throwable error) {
+ CompletableFuture<AiCliCommandExecutor.Result> future = activeCliCommand;
+ synchronized (this) {
+ if (future == null || activeCliCommand != future) {
+ return;
+ }
+ activeCliCommand = null;
+ }
+ thinking.set(false);
+ thinkingVerb = null;
Review Comment:
This unconditionally clears `thinking` and `thinkingVerb` when a CLI command completes. But CLI commands (like `/send`) are designed to run _without_ putting the panel into the thinking state, so the user can keep typing and even submit an LLM question while a CLI command is in-flight.
If a user does `/send direct:foo hello` and then immediately asks a question (which sets `thinking=true`), this callback could race and clear the thinking state while the LLM agent thread is still running — leaving the panel appearing idle with no spinner.
Consider guarding this with `if (activeCliCommand != null)` or only clearing thinking if it was set by the CLI path.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java:
##########
@@ -1126,11 +1214,41 @@ private String sendAnthropicStreamingRequest(String requestUrl, JsonObject body,
// ---- Endpoint detection helpers ----
+ // Order matters: /api/tags is checked before /v1/models, matching the original priority.
+ private static final List<Map.Entry<String, ApiType>> EXPLICIT_URL_HEALTH_CHECK_SUFFIXES = List.of(
+ Map.entry("/api/tags", ApiType.ollama),
+ Map.entry("/v1/models", ApiType.openai));
+
private boolean tryExplicitUrl() {
if (url == null || url.isBlank()) {
return false;
}
- return isEndpointReachable(url);
+ for (Map.Entry<String, ApiType> check : EXPLICIT_URL_HEALTH_CHECK_SUFFIXES) {
+ if (tryHealthCheck(url + check.getKey())) {
+ if (apiType == null) {
+ apiType = check.getValue();
+ }
+ return true;
+ }
+ }
+ if (tryHealthCheck(url)) {
+ if (apiType == null) {
+ apiType = inferApiTypeFromUrlHost();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ ApiType inferApiTypeFromUrlHost() {
+ String lower = url.toLowerCase();
+ if (lower.contains("anthropic")) {
+ return ApiType.anthropic;
+ }
+ if (lower.contains("11434") || lower.contains("ollama")) {
+ return ApiType.ollama;
+ }
+ return ApiType.openai;
Review Comment:
The original `tryExplicitUrl()` was a pure query (just checked reachability without mutating state). This new version now silently sets `apiType` as a side effect when it was previously null, using URL-based heuristics that default to `openai` for anything that doesn't match "anthropic", "11434", or "ollama".
A few things worth considering:
- Custom proxy endpoints (e.g. `https://gateway.internal/llm`) would be classified as `openai` even if they proxy Ollama or Anthropic.
- The other callers of `isEndpointReachable()` (used by `tryOpenAi()`, `tryOllama()`, etc.) do _not_ infer `apiType`, so the detection behavior is now inconsistent depending on the entry point.
The PR body says this was verified and the heuristic only fires when `apiType` was unset. Just flagging for awareness — if this causes misclassification reports, this is the place to look.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4948170907
PR is back to draft since I've found a few issues testing.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4948526638
Follow-up review fixes in 0db17789f02e:
- Removed the hard-coded `llama3.2` model from TUI client initialization so an unset model uses the selected provider default.
- Hardened `LlmClient.listModels()` against unexpected JSON shapes: a non-array model field now returns an empty list, matching the method’s best-effort contract. Added a regression test.
- Verified endpoint detection did not gain extra health-check requests: the prior `isEndpointReachable(url)` already probed `/api/tags`, `/v1/models`, and the base URL in that order. The current code only adds API-type inference when it was unspecified.
_AI-generated by Codex on behalf of ammachado._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3565057854
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java:
##########
@@ -1126,11 +1214,41 @@ private String sendAnthropicStreamingRequest(String requestUrl, JsonObject body,
// ---- Endpoint detection helpers ----
+ // Order matters: /api/tags is checked before /v1/models, matching the original priority.
+ private static final List<Map.Entry<String, ApiType>> EXPLICIT_URL_HEALTH_CHECK_SUFFIXES = List.of(
+ Map.entry("/api/tags", ApiType.ollama),
+ Map.entry("/v1/models", ApiType.openai));
+
private boolean tryExplicitUrl() {
if (url == null || url.isBlank()) {
return false;
}
- return isEndpointReachable(url);
+ for (Map.Entry<String, ApiType> check : EXPLICIT_URL_HEALTH_CHECK_SUFFIXES) {
+ if (tryHealthCheck(url + check.getKey())) {
+ if (apiType == null) {
+ apiType = check.getValue();
+ }
+ return true;
+ }
+ }
+ if (tryHealthCheck(url)) {
+ if (apiType == null) {
+ apiType = inferApiTypeFromUrlHost();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ ApiType inferApiTypeFromUrlHost() {
+ String lower = url.toLowerCase();
+ if (lower.contains("anthropic")) {
+ return ApiType.anthropic;
+ }
+ if (lower.contains("11434") || lower.contains("ollama")) {
+ return ApiType.ollama;
+ }
+ return ApiType.openai;
Review Comment:
Fixed in f6008c32e376. Auto-detection now identifies Ollama from its root `GET` response (`Ollama is running`) rather than URL host/port heuristics, including no longer relying on port `11434`. Explicitly configured API types retain the existing reachability check.
_AI-generated by Codex on behalf of ammachado._
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -323,14 +443,200 @@ boolean handleKeyEvent(KeyEvent ke) {
return true;
}
- private void submitQuestion() {
- String question = inputBuffer.toString().trim();
+ /**
+ * Completes the slash command name at the cursor. With multiple matches, TAB first fills in the longest common
+ * prefix; once no further prefix can be added it cycles forward through the matches (wrapping so every match is
+ * reachable with TAB alone). A single match is completed fully and a trailing space is appended. Shift+TAB cycles
+ * backward. TAB is a no-op unless the buffer is a partial command name (starts with {@code /}, no arguments yet).
+ */
+ private void handleTabCompletion(boolean backward) {
+ String text = inputBuffer.toString();
+ boolean continuing = text.equals(completionSnapshot) && completionMatches != null && completionMatches.size() > 1;
+ if (continuing) {
+ int size = completionMatches.size();
+ if (completionCycleIndex < 0) {
+ completionCycleIndex = backward ? size - 1 : 0;
+ } else {
+ completionCycleIndex = backward
+ ? (completionCycleIndex - 1 + size) % size
+ : (completionCycleIndex + 1) % size;
+ }
+ applyCompletionToken(completionMatches.get(completionCycleIndex), false);
+ return;
+ }
+
+ List<String> names = slashCommands.completionsFor(text).stream()
+ .map(AiSlashCommandRegistry.Descriptor::name)
+ .toList();
+ if (names.isEmpty()) {
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ if (names.size() == 1) {
+ applyCompletionToken(names.get(0), true);
+ // A single completion ends with a trailing space, so there is nothing left to cycle.
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ String currentToken = text.substring(1);
+ String prefix = longestCommonPrefix(names);
+ completionMatches = names;
+ if (prefix.length() > currentToken.length()) {
+ applyCompletionToken(prefix, false);
+ completionCycleIndex = -1;
+ } else {
+ completionCycleIndex = backward ? names.size() - 1 : 0;
+ applyCompletionToken(names.get(completionCycleIndex), false);
+ }
+ }
+
+ private void applyCompletionToken(String token, boolean trailingSpace) {
+ inputBuffer.setLength(0);
+ inputBuffer.append('/').append(token);
+ if (trailingSpace) {
+ inputBuffer.append(' ');
+ }
+ cursorPos = inputBuffer.length();
+ completionSnapshot = inputBuffer.toString();
+ }
+
+ private static String longestCommonPrefix(List<String> values) {
+ String prefix = values.get(0);
+ for (int i = 1; i < values.size() && !prefix.isEmpty(); i++) {
+ String value = values.get(i);
+ int max = Math.min(prefix.length(), value.length());
+ int j = 0;
+ while (j < max && prefix.charAt(j) == value.charAt(j)) {
+ j++;
+ }
+ prefix = prefix.substring(0, j);
+ }
+ return prefix;
+ }
+
+ private void submitInput() {
+ String input = inputBuffer.toString().trim();
inputBuffer.setLength(0);
cursorPos = 0;
scrollOffset = 0;
+ if (input.startsWith("/")) {
+ executeSlashCommand(input);
+ } else {
+ submitQuestion(input);
+ }
+ }
+
+ private void executeSlashCommand(String input) {
+ Optional<AiSlashCommandRegistry.ParsedCommand> parsed = slashCommands.parse(input);
+ if (parsed.isPresent() && thinking.get()) {
+ String name = parsed.get().descriptor().name();
+ if ("provider".equals(name) || "model".equals(name)) {
+ conversation.add(new ConversationEntry(
+ AiRole.SYSTEM,
+ "Wait for the current operation to finish before changing provider or model."));
+ return;
+ }
+ }
+
+ AiSlashCommandRegistry.CommandResult result = slashCommands.execute(input, slashCommandContext);
+ if (result.cliRequest() != null) {
+ AiCliCommandExecutor.Request request = result.cliRequest();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "Running " + request.displayText()));
+ // Runs in the background without entering the panel's "thinking" state, so the user can keep typing
+ // and asking questions while the command runs. Esc still cancels it via interruptBusyOperation().
+ CompletableFuture<AiCliCommandExecutor.Result> future = slashCommandContext.executeCli(request);
+ activeCliCommand = future;
+ future.whenComplete(this::handleCliCompletion);
+ return;
+ }
+ if (result.text() != null && !result.text().isBlank()) {
+ conversation.add(new ConversationEntry(result.role(), result.text()));
+ }
+ }
+
+ private void handleCliCompletion(AiCliCommandExecutor.Result cliResult, Throwable error) {
+ CompletableFuture<AiCliCommandExecutor.Result> future = activeCliCommand;
+ synchronized (this) {
+ if (future == null || activeCliCommand != future) {
+ return;
+ }
+ activeCliCommand = null;
+ }
+ thinking.set(false);
+ thinkingVerb = null;
Review Comment:
Fixed in f6008c32e376. CLI completion no longer changes `thinking` or `thinkingVerb`; the new regression test starts `/send`, then an LLM request, and verifies completion of the CLI command leaves the LLM activity state intact.
_AI-generated by Codex on behalf of ammachado._
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -323,14 +443,200 @@ boolean handleKeyEvent(KeyEvent ke) {
return true;
}
- private void submitQuestion() {
- String question = inputBuffer.toString().trim();
+ /**
+ * Completes the slash command name at the cursor. With multiple matches, TAB first fills in the longest common
+ * prefix; once no further prefix can be added it cycles forward through the matches (wrapping so every match is
+ * reachable with TAB alone). A single match is completed fully and a trailing space is appended. Shift+TAB cycles
+ * backward. TAB is a no-op unless the buffer is a partial command name (starts with {@code /}, no arguments yet).
+ */
+ private void handleTabCompletion(boolean backward) {
+ String text = inputBuffer.toString();
+ boolean continuing = text.equals(completionSnapshot) && completionMatches != null && completionMatches.size() > 1;
+ if (continuing) {
+ int size = completionMatches.size();
+ if (completionCycleIndex < 0) {
+ completionCycleIndex = backward ? size - 1 : 0;
+ } else {
+ completionCycleIndex = backward
+ ? (completionCycleIndex - 1 + size) % size
+ : (completionCycleIndex + 1) % size;
+ }
+ applyCompletionToken(completionMatches.get(completionCycleIndex), false);
+ return;
+ }
+
+ List<String> names = slashCommands.completionsFor(text).stream()
+ .map(AiSlashCommandRegistry.Descriptor::name)
+ .toList();
+ if (names.isEmpty()) {
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ if (names.size() == 1) {
+ applyCompletionToken(names.get(0), true);
+ // A single completion ends with a trailing space, so there is nothing left to cycle.
+ completionMatches = null;
+ completionCycleIndex = -1;
+ completionSnapshot = null;
+ return;
+ }
+ String currentToken = text.substring(1);
+ String prefix = longestCommonPrefix(names);
+ completionMatches = names;
+ if (prefix.length() > currentToken.length()) {
+ applyCompletionToken(prefix, false);
+ completionCycleIndex = -1;
+ } else {
+ completionCycleIndex = backward ? names.size() - 1 : 0;
+ applyCompletionToken(names.get(completionCycleIndex), false);
+ }
+ }
+
+ private void applyCompletionToken(String token, boolean trailingSpace) {
+ inputBuffer.setLength(0);
+ inputBuffer.append('/').append(token);
+ if (trailingSpace) {
+ inputBuffer.append(' ');
+ }
+ cursorPos = inputBuffer.length();
+ completionSnapshot = inputBuffer.toString();
+ }
+
+ private static String longestCommonPrefix(List<String> values) {
+ String prefix = values.get(0);
+ for (int i = 1; i < values.size() && !prefix.isEmpty(); i++) {
+ String value = values.get(i);
+ int max = Math.min(prefix.length(), value.length());
+ int j = 0;
+ while (j < max && prefix.charAt(j) == value.charAt(j)) {
+ j++;
+ }
+ prefix = prefix.substring(0, j);
+ }
+ return prefix;
+ }
+
+ private void submitInput() {
+ String input = inputBuffer.toString().trim();
inputBuffer.setLength(0);
cursorPos = 0;
scrollOffset = 0;
+ if (input.startsWith("/")) {
+ executeSlashCommand(input);
+ } else {
+ submitQuestion(input);
+ }
+ }
+
+ private void executeSlashCommand(String input) {
+ Optional<AiSlashCommandRegistry.ParsedCommand> parsed = slashCommands.parse(input);
+ if (parsed.isPresent() && thinking.get()) {
+ String name = parsed.get().descriptor().name();
+ if ("provider".equals(name) || "model".equals(name)) {
+ conversation.add(new ConversationEntry(
+ AiRole.SYSTEM,
+ "Wait for the current operation to finish before changing provider or model."));
+ return;
+ }
+ }
+
+ AiSlashCommandRegistry.CommandResult result = slashCommands.execute(input, slashCommandContext);
+ if (result.cliRequest() != null) {
+ AiCliCommandExecutor.Request request = result.cliRequest();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "Running " + request.displayText()));
+ // Runs in the background without entering the panel's "thinking" state, so the user can keep typing
+ // and asking questions while the command runs. Esc still cancels it via interruptBusyOperation().
+ CompletableFuture<AiCliCommandExecutor.Result> future = slashCommandContext.executeCli(request);
+ activeCliCommand = future;
+ future.whenComplete(this::handleCliCompletion);
+ return;
+ }
+ if (result.text() != null && !result.text().isBlank()) {
+ conversation.add(new ConversationEntry(result.role(), result.text()));
+ }
+ }
+
+ private void handleCliCompletion(AiCliCommandExecutor.Result cliResult, Throwable error) {
+ CompletableFuture<AiCliCommandExecutor.Result> future = activeCliCommand;
+ synchronized (this) {
+ if (future == null || activeCliCommand != future) {
+ return;
+ }
+ activeCliCommand = null;
+ }
+ thinking.set(false);
+ thinkingVerb = null;
+
+ if (error != null) {
+ String displayText = cliResult != null ? cliResult.displayText() : "command";
+ conversation.add(new ConversationEntry(
+ AiRole.ERROR, "Failed to run " + displayText + ": " + error.getMessage()));
+ scrollOffset = 0;
+ return;
+ }
+
+ if (cliResult.exitCode() == 0) {
+ conversation.add(new ConversationEntry(
+ AiRole.SYSTEM,
+ cliResult.displayText() + " completed in " + cliResult.elapsedMs() + " ms\n\n" + cliResult.output()));
+ } else {
+ conversation.add(new ConversationEntry(
+ AiRole.ERROR,
+ cliResult.displayText() + " exit code " + cliResult.exitCode() + "\n\n" + cliResult.output()));
+ }
+ scrollOffset = 0;
+ }
+
+ private void interruptBusyOperation() {
+ if (activeCliCommand != null) {
+ activeCliCommand = null;
+ slashCommandContext.cancelCli();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "(command cancelled)"));
+ thinking.set(false);
+ thinkingVerb = null;
+ } else {
+ stopAgentThread();
+ conversation.add(new ConversationEntry(AiRole.SYSTEM, "(cancelled)"));
+ }
+ }
+
+ private void stopAgentThread() {
+ Thread t = agentThread;
+ if (t == null) {
+ return;
+ }
+ if (t != Thread.currentThread()) {
+ t.interrupt();
+ // Unlike AiCliCommandExecutor.cancel(), this join is not offloaded to a watcher thread: it runs
+ // on the caller, which is either the interactive UI thread (Esc/Ctrl+C via interruptBusyOperation)
+ // or destroy() during shutdown. If the LLM HTTP call in runAgentLoop() ignores the interrupt (e.g.
+ // a client/library that doesn't check the interrupt flag mid-request), this blocks that caller for
+ // up to 30s. Left blocking deliberately for now because AiPanelTest asserts synchronous
+ // cancellation semantics (isAgentThreadRunningForTesting() is false immediately after Esc); making
+ // this async would need those tests reworked to poll instead.
+ try {
+ t.join(30_000);
Review Comment:
Fixed in f6008c32e376. Agent cancellation interrupts immediately and moves the bounded `join` to a daemon watcher, so the TUI event thread is not blocked. The tests now wait with Awaitility rather than expecting synchronous thread termination.
_AI-generated by Codex on behalf of ammachado._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4940372852
<!-- ci-tested-modules -->
:test_tube: **CI tested the following changed modules:**
- `docs`
- `dsl/camel-jbang/camel-jbang-core`
- `dsl/camel-jbang/camel-jbang-plugin-tui`
---
<details><summary>:microscope: Scalpel shadow comparison — Scalpel: 8 tested, 8 compile-only — current: 6 all tested</summary>
[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **16 affected modules** (current approach: 6).
<details><summary>:warning: Modules only in Scalpel (10)</summary>
- `camel-jbang-core`
- `camel-jbang-it`
- `camel-jbang-main`
- `camel-jbang-plugin-edit`
- `camel-jbang-plugin-generate`
- `camel-jbang-plugin-kubernetes`
- `camel-jbang-plugin-test`
- `camel-launcher`
- `coverage`
- `docs`
</details>
**Skip-tests mode would test 8 modules** (3 direct + 5 downstream), **skip tests for 8** (generated code, meta-modules)
<details><summary>Modules Scalpel would test (8)</summary>
- `camel-jbang-core`
- `camel-jbang-mcp`
- `camel-jbang-plugin-mcp`
- `camel-jbang-plugin-route-parser`
- `camel-jbang-plugin-tui`
- `camel-jbang-plugin-validate`
- `camel-launcher-container`
- `docs`
</details>
<details><summary>Modules with tests skipped (8)</summary>
- `camel-jbang-it`
- `camel-jbang-main`
- `camel-jbang-plugin-edit`
- `camel-jbang-plugin-generate`
- `camel-jbang-plugin-kubernetes`
- `camel-jbang-plugin-test`
- `camel-launcher`
- `coverage`
</details>
> :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
</details>
:warning: **Some tests are disabled on GitHub Actions** (`@DisabledIfSystemProperty(named = "ci.env.name")`) and require manual verification:
- `dsl/camel-jbang/camel-jbang-core`: 1 test(s) disabled on GitHub Actions
:bulb: **Manual integration tests recommended:**
> You modified `dsl/camel-jbang/camel-jbang-core`. The related integration tests in `dsl/camel-jbang/camel-jbang-it` are excluded from CI. Consider running them manually:
> ```
> mvn verify -f dsl/camel-jbang/camel-jbang-it -Djbang-it-test
> ```
<details><summary>All tested modules (16 modules)</summary>
- Camel :: Coverage
- Camel :: Docs
- Camel :: JBang :: Core
- Camel :: JBang :: Integration tests
- Camel :: JBang :: MCP
- Camel :: JBang :: Main
- Camel :: JBang :: Plugin :: Edit
- Camel :: JBang :: Plugin :: Generate
- Camel :: JBang :: Plugin :: Kubernetes
- Camel :: JBang :: Plugin :: MCP
- Camel :: JBang :: Plugin :: Route Parser
- Camel :: JBang :: Plugin :: TUI
- Camel :: JBang :: Plugin :: Testing
- Camel :: JBang :: Plugin :: Validate
- Camel :: Launcher
- Camel :: Launcher :: Container
</details>
---
:gear: [View full build and test results](https://github.com/apache/camel/actions/runs/29129387488)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4940223820
:star2: Thank you for your contribution to the Apache Camel project! :star2:
:robot: CI automation will test this PR automatically.
:camel: Apache Camel Committers, please review the following items:
* First-time contributors **require MANUAL approval** for the GitHub Actions to run
* You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
* You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
* Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
:warning: Be careful when sharing logs. Review their contents before sharing them publicly.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #24594:
URL: https://github.com/apache/camel/pull/24594#discussion_r3563387846
##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java:
##########
@@ -83,21 +94,37 @@ record LogEntry(String timestamp, LogLevel level, String message, String detail)
private final StringBuilder inputBuffer = new StringBuilder();
private int cursorPos;
- // Conversation display
+ // Conversation display. CopyOnWriteArrayList because entries are appended from the agent thread and the
+ // CLI-command-completion callback while the render thread iterates the list concurrently.
private final List<ConversationEntry> conversation = new CopyOnWriteArrayList<>();
private int scrollOffset;
// LLM state
- private LlmClient client;
+ private volatile LlmClient client;
private List<LlmClient.Message> messages;
private List<LlmClient.ToolDef> tools;
private AskTools askTools;
private final AtomicBoolean thinking = new AtomicBoolean();
private volatile Thread agentThread;
private String initError;
private long thinkingStartTime;
+ private String thinkingVerb;
private volatile int sessionTotalTokens;
Review Comment:
**Data race on `thinkingVerb`:** This field is a plain `private String` written from the UI thread (in `submitQuestion` / `executeSlashCommand`) and read/written from the `CompletableFuture` completion callback thread (in `handleCliCompletion`). Without `volatile`, the JMM does not guarantee visibility across threads.
Practically harmless (display-only), but easy to fix:
```suggestion
private volatile String thinkingVerb;
```
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java:
##########
@@ -287,6 +287,95 @@ public ChatResponse chatWithTools(String systemPrompt, List<Message> messages, L
};
}
+ // -- Model listing --
+
+ /**
+ * Lists model identifiers available from the configured provider: Ollama's {@code /api/tags}, or the
+ * OpenAI-compatible/Anthropic {@code /v1/models}. Vertex AI has no equivalent listing endpoint (models come from
+ * the Vertex AI model garden, not the Anthropic API surface), so it always returns an empty list. Best-effort: any
+ * failure (unreachable endpoint, non-200 response, unexpected JSON shape) yields an empty list rather than
+ * throwing, since this only feeds the informational {@code /model} listing in the AI panel.
+ */
+ public List<String> listModels() {
+ if (apiType == null || url == null || url.isBlank()) {
Review Comment:
**Behavioral change in endpoint detection:** The old `tryExplicitUrl()` simply called `isEndpointReachable(url)`. The new version probes `url + "/api/tags"` and `url + "/v1/models"` first, and silently sets `apiType` as a side effect when a probe succeeds.
This could change behavior for users with custom proxy/gateway URLs that don't serve those sub-paths (the probes would fail, falling through to the bare URL check, but the extra network round-trips add latency). Worth noting in the JIRA or commit message that detection behavior changed.
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java:
##########
@@ -122,7 +122,8 @@ public Integer doCall() throws Exception {
if (!client.detectEndpoint()) {
printer().printErr("LLM service is not reachable.");
- printer().printErr("Options: --url=<endpoint>, --api-type=anthropic, or start Ollama with: camel infra run ollama");
+ printer()
Review Comment:
Nit: This is an unrelated formatter change (line wrap of `printErr`) mixed into the feature PR. Not wrong, but ideally would be a separate commit or omitted to keep the diff focused on the slash-commands feature.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "ammachado (via GitHub)" <gi...@apache.org>.
ammachado commented on PR #24594:
URL: https://github.com/apache/camel/pull/24594#issuecomment-4948008051
@gnodet Fixed in eebbb7bf0797646108895e9466695ec549c10744 — removed the duplicated placeholder block in `AiPanel.renderInput()`; the hint now renders once. I also strengthened `commandPlaceholderRendersAfterTrailingSpace` to assert the placeholder appears exactly once (the previous `contains()` assertion passed even with the duplicate). Verified the strengthened test fails on the duplicated code and passes after the fix.
_Claude Code on behalf of ammachado_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-23978: Add AI panel slash commands to Camel TUI [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #24594:
URL: https://github.com/apache/camel/pull/24594
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org