You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/10 11:48:53 UTC
(camel) branch main updated: camel-jbang-plugin-tui: ACP follow-ups for file edits and the simple helpers
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 f5395dfd3b75 camel-jbang-plugin-tui: ACP follow-ups for file edits and the simple helpers
f5395dfd3b75 is described below
commit f5395dfd3b7560efc3a8a9d9b7c70787e08a4525
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Sep 10 13:46:06 2026 +0200
camel-jbang-plugin-tui: ACP follow-ups for file edits and the simple helpers
- tui_write_file is approved at the ACP layer while the write mode is
confirm or live: the facade's diff dialog or the live replay asks the
user anyway, so the permission popup made them answer twice. With
/write auto nothing else asks and the popup stays.
- tui_eval_expression joins the read-only tools: it evaluates against a
scratch exchange and sends nothing through a route, so the agent can
check simple expressions without a popup for each.
- The ACP preamble tells the agent to edit route sources only through
tui_write_file, never with its own file tools, so the user sees the
diff or watches the edit; when the agent still asks to edit a file
directly, the permission popup says the TUI cannot show or replay it
and that Esc sends the agent back to tui_write_file.
- F1 help and F2 Setup AI describe the acp:* presets, the on-demand MCP
server and the permission rules; the user manual permissions
paragraph matches.
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../modules/ROOT/pages/camel-jbang-tui.adoc | 17 +++--
.../core/commands/tui/AcpPermissionPopup.java | 12 +++-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 72 +++++++++++++++-------
.../jbang/core/commands/tui/DocViewerPopup.java | 29 ++++++++-
.../jbang/core/commands/tui/TuiToolRegistry.java | 6 +-
.../src/main/resources/tui/help/overview.md | 8 ++-
.../core/commands/tui/AcpPermissionPopupTest.java | 14 +++++
.../jbang/core/commands/tui/AiPanelAcpTest.java | 45 ++++++++++++++
8 files changed, 168 insertions(+), 35 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 ac2c78c22f4a..a1641ee38839 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -1052,12 +1052,17 @@ required. *F2* -> _MCP Info_ shows the port and the tool calls the agent makes.
`--mcp`, it is bound to `127.0.0.1` with no authentication, and it rejects requests that carry an `Origin`
header (so a web page cannot reach it) or that are not JSON.
-Permissions: calls to the TUI's read-only tools (the `tui_get_*` tools, catalog documentation, locate, validate)
-are approved without asking. A tool that changes anything, such as `tui_control`, `tui_send_message` or
-`tui_execute_sql`, opens the same popup as any other request; answering "Always allow" makes it a one-time
-question per tool. For anything else the agent wants to do,
-such as editing a file or running a command, a popup shows the agent's options; *Enter* selects, *Esc* rejects
-that one call and lets the turn continue, and *Ctrl+C* cancels the whole turn.
+Permissions: calls to the TUI's read-only tools (the `tui_get_*` tools, catalog documentation, expression
+evaluation, locate, validate) are approved without asking. So is `tui_write_file` while the write mode is
+`confirm` or `live`, because the TUI itself asks before the file is touched (the diff dialog or the live replay
+in the Source editor); with `/write auto` the permission popup is the only question and stays. A tool that
+changes anything else, such as `tui_control`, `tui_send_message` or `tui_execute_sql`, opens the same popup as
+any other request; answering "Always allow" makes it a one-time question per tool. For anything else the agent
+wants to do, such as editing a file with its own tools or running a command, a popup shows the agent's options;
+*Enter* selects, *Esc* rejects that one call and lets the turn continue, and *Ctrl+C* cancels the whole turn.
+The preamble tells the agent to edit route sources through `tui_write_file` rather than its own file tools, so
+that you see the diff or watch the edit; when it still asks to edit a file directly, the popup says so and *Esc*
+sends it back to `tui_write_file`.
"Always allow" choices are remembered by the agent for the session. The model, reasoning settings and any
"always allow" rules are configured in the agent, not in the TUI; `/model` only reports the agent in use.
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopup.java
index aeeb893f63bb..da9c575b1c4e 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopup.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopup.java
@@ -61,6 +61,7 @@ final class AcpPermissionPopup {
private volatile boolean visible;
private String title = "";
private String kind = "";
+ private String hint;
private List<String> inputLines = List.of();
private List<JsonObject> options = List.of();
private final ListState listState = new ListState();
@@ -76,8 +77,14 @@ final class AcpPermissionPopup {
}
void open(JsonObject toolCall, List<JsonObject> options) {
+ open(toolCall, options, null);
+ }
+
+ /** {@code hint} is an extra line shown under the kind, for example why the user may want to reject the call. */
+ void open(JsonObject toolCall, List<JsonObject> options, String hint) {
this.title = String.valueOf(toolCall.getStringOrDefault("title", "Use tool?"));
this.kind = String.valueOf(toolCall.getStringOrDefault("kind", "other"));
+ this.hint = hint;
this.inputLines = formatInput(toolCall.get("rawInput"));
this.options = new ArrayList<>(options);
listState.selectFirst();
@@ -143,7 +150,7 @@ final class AcpPermissionPopup {
}
void render(Frame frame, Rect area) {
- int headerRows = 2 + inputLines.size() + 1;
+ int headerRows = 2 + (hint != null ? 1 : 0) + inputLines.size() + 1;
int popupW = Math.max(20, Math.min(80, area.width() - 4));
int popupH = Math.max(6, Math.min(2 + headerRows + options.size(), area.height() - 2));
Rect popup = DialogHelper.centered(area, popupW, popupH);
@@ -164,6 +171,9 @@ final class AcpPermissionPopup {
List<Line> header = new ArrayList<>();
header.add(Line.from(Span.styled(title, Style.EMPTY.bold())));
header.add(Line.from(Span.styled("kind: " + kind, Style.EMPTY.dim())));
+ if (hint != null) {
+ header.add(Line.from(Span.styled(hint, Theme.warning())));
+ }
for (String line : inputLines) {
header.add(Line.from(Span.styled(line, Style.EMPTY.dim())));
}
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 02eee700d536..b1603543f97e 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
@@ -1656,7 +1656,7 @@ class AiPanel {
private void runAcpTurn(String question) throws IOException {
AcpAgentClient agent = ensureAcpSession();
boolean agentCommand = question.startsWith("/");
- String text = acpPreambleSent || agentCommand ? question : buildSystemPrompt() + "\n\n" + question;
+ String text = acpPreambleSent || agentCommand ? question : acpPreamble() + "\n\n" + question;
AcpTurnListener listener = new AcpTurnListener();
String stopReason = agent.prompt(acpSessionId, text, listener);
if (!agentCommand) {
@@ -1910,7 +1910,13 @@ class AiPanel {
String name = String.valueOf(toolCall.getStringOrDefault("name", ""));
String title = String.valueOf(toolCall.getStringOrDefault("title", ""));
String kind = String.valueOf(toolCall.getStringOrDefault("kind", ""));
- if (isTuiTool(name, title, kind)) {
+ String tool = tuiToolName(name, title, kind);
+ boolean readOnly = tool != null && TuiToolRegistry.READ_ONLY_TOOLS.contains(tool);
+ // the facade asks the user before a tui_write_file call touches a file (the confirm dialog or the live
+ // replay), so a second question at the ACP layer would only make the user answer twice; in auto mode
+ // nothing else asks and the popup stays
+ boolean confirmedByTui = "tui_write_file".equals(tool) && writeMode != McpFacade.WriteMode.AUTO;
+ if (readOnly || confirmedByTui) {
String optionId = firstOptionOfKind(options, "allow_always");
if (optionId == null) {
optionId = firstOptionOfKind(options, "allow_once");
@@ -1918,12 +1924,14 @@ class AiPanel {
if (optionId == null && !options.isEmpty()) {
optionId = options.get(0).getString("optionId");
}
- log(LogLevel.TOOL, "Auto-approved read-only TUI tool", title);
+ log(LogLevel.TOOL, readOnly
+ ? "Auto-approved read-only TUI tool"
+ : "Auto-approved tui_write_file (the TUI confirms the write itself)", title);
return optionId;
}
CompletableFuture<String> decision = new CompletableFuture<>();
pendingPermission = decision;
- permissionPopup.open(toolCall, options);
+ permissionPopup.open(toolCall, options, permissionHint(kind));
log(LogLevel.TOOL, "Permission requested", title);
try {
return decision.get();
@@ -1940,26 +1948,36 @@ class AiPanel {
}
/**
- * Only a call to one of the TUI's {@link TuiToolRegistry#READ_ONLY_TOOLS} counts as auto-approvable: the name the
- * Claude adapter sends ({@code mcp__camel-tui__tui_get_state}) or a title of the form
- * {@code tui_get_state (camel-tui MCP Server)}. A TUI tool that changes anything is not in that set and goes to the
- * user. Kinds that touch files or run commands never qualify, whatever the title says: a path containing
- * "camel-tui" is not a tool identity.
+ * The camel-tui tool a permission request is about, or null when it is not one: the name the Claude adapter sends
+ * ({@code mcp__camel-tui__tui_get_state}) or a title of the form {@code tui_get_state (camel-tui MCP Server)}.
+ * Kinds that touch files or run commands never qualify, whatever the title says: a path containing "camel-tui" is
+ * not a tool identity. The caller decides what the tool may do: only {@link TuiToolRegistry#READ_ONLY_TOOLS} and a
+ * write the TUI confirms itself are approved silently, everything else goes to the user.
*/
- private boolean isTuiTool(String name, String title, String kind) {
+ private static String tuiToolName(String name, String title, String kind) {
if (FILE_OR_SHELL_KINDS.contains(kind)) {
- return false;
+ return null;
}
- String tool = null;
if (name.startsWith(TUI_TOOL_PREFIX)) {
- tool = name.substring(TUI_TOOL_PREFIX.length());
- } else {
- int paren = title.indexOf("(camel-tui");
- if (paren > 0) {
- tool = title.substring(0, paren).strip();
- }
+ return name.substring(TUI_TOOL_PREFIX.length());
+ }
+ int paren = title.indexOf("(camel-tui");
+ if (paren > 0) {
+ return title.substring(0, paren).strip();
+ }
+ return null;
+ }
+
+ /**
+ * The line the permission popup shows under the tool: an agent that edits a source file with its own tools bypasses
+ * the TUI's diff and live replay, and the user may prefer to reject it and have the agent use tui_write_file, as
+ * the preamble told it to.
+ */
+ private static String permissionHint(String kind) {
+ if ("edit".equals(kind) || "delete".equals(kind) || "move".equals(kind)) {
+ return "The TUI cannot show or replay this change; Esc rejects it so the agent uses tui_write_file";
}
- return tool != null && TuiToolRegistry.READ_ONLY_TOOLS.contains(tool);
+ return null;
}
private static String firstOptionOfKind(List<JsonObject> options, String kind) {
@@ -2843,6 +2861,18 @@ class AiPanel {
""";
}
+ /**
+ * The preamble an ACP agent gets ahead of its first prompt: the panel's system prompt plus the rules that only
+ * matter for an agent with file tools of its own. Not part of the prompt budget, which covers the LLM path.
+ */
+ private String acpPreamble() {
+ String prompt = buildSystemPrompt();
+ return (prompt.endsWith("\n") ? prompt : prompt + "\n")
+ + "- You run inside the Camel TUI: edit the integration's source files only with tui_write_file, "
+ + "never with your own file tools, so the user sees the diff and confirms it, or watches the edit "
+ + "being typed in the Source editor (/write live)\n";
+ }
+
/**
* The static prefix sent with every request. It deliberately contains nothing that changes between turns (the
* selected integration travels in the user message instead) so a local model's prompt cache can reuse it, and it
@@ -3164,7 +3194,7 @@ class AiPanel {
}
sb.append("Agent commands: ").append(agent != null ? agent.availableCommands().size() : 0)
.append(" (/agent: lists them)\n");
- sb.append("Preamble: ~").append(LlmClient.formatTokens(estimateTokens(buildSystemPrompt().length())))
+ sb.append("Preamble: ~").append(LlmClient.formatTokens(estimateTokens(acpPreamble().length())))
.append(" tokens, sent once per session ahead of the first prompt (/prompt shows it); ")
.append(acpPreambleSent ? "sent" : "not sent yet").append('\n');
if (acpContextUsed > 0) {
@@ -3692,7 +3722,7 @@ class AiPanel {
@Override
public String systemPrompt() {
if (acpPreset != null) {
- return "Sent to " + acpLabel() + " ahead of the first prompt of each session:\n\n" + buildSystemPrompt();
+ return "Sent to " + acpLabel() + " ahead of the first prompt of each session:\n\n" + acpPreamble();
}
return buildSystemPrompt();
}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
index 9065f7eb328e..75bfce6ced89 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DocViewerPopup.java
@@ -392,7 +392,8 @@ class DocViewerPopup {
} else if (ollama.running()) {
status = "**Detected:** Ollama is running but has no models. Run `ollama pull qwen3.6:35b-a3b`.";
} else {
- status = "**Status:** No AI provider detected. Set an API key or start Ollama, then press F8.";
+ status = "**Status:** No AI provider detected. Set an API key, start Ollama, or pick a coding agent "
+ + "with Ctrl+P in the panel (section 5), then press F8.";
}
String url = "http://localhost:" + mcpPort + "/mcp";
openMarkdown("Setup AI",
@@ -457,9 +458,31 @@ class DocViewerPopup {
+ "- **Ctrl+U** toggles the usage view with token consumption\n"
+ "- `/help` lists all slash commands; **↑/↓** recalls earlier prompts\n\n"
+ "Check **F2 → Run Doctor** to verify the detected provider and Ollama models.\n\n"
- + "## 5. Let an AI coding agent drive the TUI (optional)\n\n"
+ + "## 5. Use your coding agent inside the panel (ACP)\n\n"
+ + "If you already use Claude Code, Codex, OpenCode, IBM Bob, Qwen Code or DeepSeek Harness,\n"
+ + "the panel can hand your questions to it over the Agent Client Protocol. No API key is\n"
+ + "needed in the TUI: the agent keeps its own login, model, memory and skills.\n\n"
+ + "Press **Ctrl+P** in the panel and pick a preset:\n\n"
+ + "| Provider | Runs | Before the first question |\n"
+ + "|----------|------|---------------------------|\n"
+ + "| `acp:claude` | `npx -y @agentclientprotocol/claude-agent-acp` | log in with the `claude` CLI |\n"
+ + "| `acp:codex` | `npx -y @agentclientprotocol/codex-acp` | `codex login` |\n"
+ + "| `acp:opencode` | `opencode acp` | `opencode auth login` |\n"
+ + "| `acp:bob` | `bob acp` | set `BOBSHELL_API_KEY` or run `bob` once |\n"
+ + "| `acp:qwen` | `qwen --acp` | set `OPENAI_API_KEY` and `OPENAI_BASE_URL` |\n"
+ + "| `acp:dsh` | `npx -y @deepseek-ai/dsh --profile acp` | configure the model key in DeepSeek Harness |\n"
+ + "| `acp:custom` | *ACP Command* in **F2 → Settings** | depends on the agent |\n\n"
+ + "The `npx` presets need Node.js 22 or newer. The agent starts with your first question and\n"
+ + "gets the TUI's tools through an MCP server that is started on demand, so `--mcp` is not\n"
+ + "required. Read-only tools are approved automatically; a tool that changes something, or\n"
+ + "a file edit or command of the agent's own, opens a permission popup first (**Enter**\n"
+ + "allows, **Esc** rejects, **Ctrl+C** cancels the turn). File edits go through the same\n"
+ + "`/write confirm|auto|live` modes as the built-in providers. The agent's skills and\n"
+ + "commands appear as `/agent:<name>`. Set *AI Provider* to the preset in **F2 → Settings**\n"
+ + "to make it the default.\n\n"
+ + "## 6. Connect an agent from outside over MCP (optional)\n\n"
+ "Start the TUI with `--mcp` to expose an MCP server, then connect an agent such as\n"
- + "Claude Code:\n\n"
+ + "Claude Code from its own terminal:\n\n"
+ " claude mcp add --transport http camel-tui " + url + "\n\n"
+ "The footer turns green when the agent connects. See **F2 → AI & MCP → MCP Info**\n"
+ "for the available tools and a `.mcp.json` example.\n");
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 64ba553d60eb..4b821e16c1fa 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -101,10 +101,12 @@ class TuiToolRegistry {
/**
* Tools that only return information and never change the TUI, the integration or its data. The ACP permission
* handler approves calls to these without asking; anything else, including tui_control, tui_send_message and
- * tui_execute_sql, is put in front of the user.
+ * tui_execute_sql, is put in front of the user. tui_eval_expression is here because it evaluates an expression
+ * against a scratch exchange in the running integration and sends nothing through a route.
*/
static final Set<String> READ_ONLY_TOOLS = Set.of(
- "tui_catalog_doc", "tui_get_ai_log", "tui_get_diagram", "tui_get_errors", "tui_get_events",
+ "tui_catalog_doc", "tui_eval_expression", "tui_get_ai_log", "tui_get_diagram", "tui_get_errors",
+ "tui_get_events",
"tui_get_files", "tui_get_history", "tui_get_log", "tui_get_mcp_log", "tui_get_options",
"tui_get_processor_detail", "tui_get_readme", "tui_get_screen", "tui_get_spans", "tui_get_state",
"tui_get_status", "tui_get_table", "tui_get_themes", "tui_get_topology", "tui_list_examples",
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index f6b45b2f4e46..c78e0a8e20f9 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -110,8 +110,12 @@ Two panels can be opened on top of any tab. Opening one closes the other.
- `F8` — **AI Prompt**: ask questions about the running integrations in plain
English. The AI answers by calling the same tools an MCP agent uses (status,
routes, log, errors, traces, infra services). It needs an API key in the
- environment or a local Ollama model; `F2` → `AI & MCP` → `Setup AI` explains
- the options. Press `F8` again to close it and `Shift+F8` to cycle its height.
+ environment, a local Ollama model, or a coding agent you already use (Claude
+ Code, Codex, OpenCode, IBM Bob, Qwen Code, DeepSeek Harness) picked with
+ `Ctrl+P` as `acp:<name>`; that agent then runs inside the panel with its own
+ login, model and skills, and asks you before it changes anything. `F2` →
+ `AI & MCP` → `Setup AI` explains the options. Press `F8` again to close it
+ and `Shift+F8` to cycle its height.
Inside the panel `Ctrl+U` toggles the AI usage view (tokens per model and
per question, and how much time went to the model versus tool calls),
`Ctrl+P` switches provider or model, `Ctrl+Y` copies the last answer
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopupTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopupTest.java
index 0e4b27e9a58c..887d228b3c86 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopupTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AcpPermissionPopupTest.java
@@ -99,6 +99,20 @@ class AcpPermissionPopupTest {
assertNull(popup.consumeDecision(), "decision is consumed once");
}
+ @Test
+ void hintIsShownUnderTheKindWhenGiven() {
+ AcpPermissionPopup popup = new AcpPermissionPopup();
+ popup.open(toolCall(), options(), "Esc rejects it so the agent uses tui_write_file");
+ Rect area = new Rect(0, 0, 100, 30);
+ Buffer buffer = Buffer.empty(area);
+ popup.render(Frame.forTesting(buffer), area);
+ String rendered = TuiTestHelper.bufferToString(buffer);
+ assertTrue(rendered.contains("kind: edit"));
+ assertTrue(rendered.contains("Esc rejects it so the agent uses tui_write_file"));
+ assertTrue(rendered.contains("file_path"), "the input still follows the hint");
+ assertTrue(rendered.contains("Allow once"));
+ }
+
@Test
void escapePicksRejectOnceWhenOffered() {
AcpPermissionPopup popup = new AcpPermissionPopup();
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
index 9601c22d0884..99d61ca3d43c 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
@@ -562,6 +562,51 @@ class AiPanelAcpTest {
assertTrue(hasEntry(panel, AiRole.SYSTEM, "(stopped: opt-reject)"), "a path is not a tool identity");
}
+ @Test
+ void writeFileIsAutoApprovedWhenTheTuiConfirmsTheWriteItself() throws Exception {
+ AiPanel panel = acpPanel();
+ assertTrue(panel.describeWriteModeForTesting().startsWith("confirm"));
+ askPermissionDuringPrompt(permissionParams("mcp__camel-tui__tui_write_file", "tui_write_file"));
+ ask(panel, "hi");
+ awaitIdle(panel);
+ assertFalse(panel.isPermissionPopupVisibleForTesting());
+ assertTrue(hasEntry(panel, AiRole.SYSTEM, "(stopped: opt-always)"),
+ "the confirm dialog asks, a second question here would make the user answer twice");
+ }
+
+ @Test
+ void writeFileOpensThePopupWhenWritesAreAutomatic() throws Exception {
+ AiPanel panel = acpPanel();
+ ask(panel, "/write auto");
+ assertTrue(panel.describeWriteModeForTesting().startsWith("auto"));
+ askPermissionDuringPrompt(permissionParams("mcp__camel-tui__tui_write_file", "tui_write_file"));
+ ask(panel, "hi");
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertTrue(panel.isPermissionPopupVisibleForTesting()));
+ panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE, KeyModifiers.NONE));
+ awaitIdle(panel);
+ assertTrue(hasEntry(panel, AiRole.SYSTEM, "(stopped: opt-reject)"), "nothing else asks in auto mode");
+ }
+
+ @Test
+ void evalExpressionIsReadOnly() throws Exception {
+ AiPanel panel = acpPanel();
+ askPermissionDuringPrompt(permissionParams("mcp__camel-tui__tui_eval_expression", "tui_eval_expression"));
+ ask(panel, "hi");
+ awaitIdle(panel);
+ assertFalse(panel.isPermissionPopupVisibleForTesting());
+ assertTrue(hasEntry(panel, AiRole.SYSTEM, "(stopped: opt-always)"));
+ }
+
+ @Test
+ void preambleTellsTheAgentToEditThroughTheTui() throws Exception {
+ AiPanel panel = acpPanel();
+ ask(panel, "hi");
+ awaitIdle(panel);
+ String prompt = promptText(agent.received("session/prompt").get(0));
+ assertTrue(prompt.contains("only with tui_write_file"), prompt);
+ assertTrue(prompt.contains("/write live"), prompt);
+ }
+
@Test
void unregisteredToolWithCamelTuiTitleIsNotAutoApproved() throws Exception {
AiPanel panel = acpPanel();