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 e5e7e9a4c937 CAMEL-23634: camel-jbang - TUI Send Message dialog
(#23598)
e5e7e9a4c937 is described below
commit e5e7e9a4c937633e0d04a644db9f2b0e464ac9d0
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 28 14:01:45 2026 +0200
CAMEL-23634: camel-jbang - TUI Send Message dialog (#23598)
* CAMEL-23634: camel-jbang - TUI Send Message dialog
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: Use standard HINT_KEY_STYLE for Send Message popup
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: camel-jbang - TUI Send Message add headers support
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: camel-jbang - TUI Send Message clipboard paste support
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: camel-jbang - TUI fix F5 refresh on History tab
Co-Authored-By: Claude <[email protected]>
* CAMEL-23634: Fix F5 refresh in History tab
F5 now triggers a one-shot refresh of history data instead of being a no-op.
Co-Authored-By: Claude <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 82 ++-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 17 +-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 5 +
.../dsl/jbang/core/commands/tui/RoutesTab.java | 13 +
.../jbang/core/commands/tui/SendMessagePopup.java | 614 +++++++++++++++++++++
5 files changed, 724 insertions(+), 7 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 0ecdd69acbd8..2b65b0441824 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -66,8 +67,9 @@ class ActionsPopup {
private static final int ACTION_CLASSPATH = 8;
private static final int ACTION_MCP_INFO = 9;
private static final int ACTION_MCP_LOG = 10;
- private static final int ACTION_RESET_STATS = 11;
- private static final int ACTION_STOP_ALL = 12;
+ private static final int ACTION_SEND_MESSAGE = 11;
+ private static final int ACTION_RESET_STATS = 12;
+ private static final int ACTION_STOP_ALL = 13;
private final Supplier<Set<String>> runningNames;
private final Supplier<List<IntegrationInfo>> integrations;
@@ -107,14 +109,17 @@ class ActionsPopup {
private final DoctorPopup doctorPopup = new DoctorPopup();
private final ClasspathPopup classpathPopup = new ClasspathPopup();
+ private final SendMessagePopup sendMessagePopup = new SendMessagePopup();
private final StopAllPopup stopAllPopup;
private final CaptionOverlay captionOverlay;
+ private ScheduledExecutorService scheduler;
private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
private String launchNotification;
private boolean launchNotificationError;
private long launchNotificationExpiry;
private volatile String pendingAutoSelect;
+ private String preSelectedRouteId;
ActionsPopup(Supplier<Set<String>> runningNames,
Supplier<List<IntegrationInfo>> integrations,
Supplier<List<InfraInfo>> infraServices, CaptionOverlay
captionOverlay,
@@ -135,6 +140,14 @@ class ActionsPopup {
this.ctx = ctx;
}
+ void setScheduler(ScheduledExecutorService scheduler) {
+ this.scheduler = scheduler;
+ }
+
+ void setPreSelectedRouteId(String routeId) {
+ this.preSelectedRouteId = routeId;
+ }
+
void setResetStatsAction(Runnable resetStatsAction) {
this.resetStatsAction = resetStatsAction;
}
@@ -149,13 +162,13 @@ class ActionsPopup {
}
private int actionCount() {
- return mcpEnabled ? 13 : 11;
+ return mcpEnabled ? 14 : 12;
}
boolean isVisible() {
return showActionsMenu || showExampleBrowser ||
runOptionsForm.isVisible() || showDocPicker || showDocViewer
|| mcpLogPopup.isVisible() || doctorPopup.isVisible() ||
classpathPopup.isVisible()
- || stopAllPopup.isVisible() || captionOverlay.isInlineMode();
+ || sendMessagePopup.isVisible() || stopAllPopup.isVisible() ||
captionOverlay.isInlineMode();
}
SelectionContext getSelectionContext() {
@@ -201,11 +214,12 @@ class ActionsPopup {
labels.add("Tape Recording Guide");
labels.add("Run Doctor");
labels.add("Show Classpath");
- labels.add("Reset Stats");
if (mcpEnabled) {
labels.add("MCP Info");
labels.add("MCP Log");
}
+ labels.add("Send Message");
+ labels.add("Reset Stats");
labels.add("Stop All");
return labels;
}
@@ -224,6 +238,7 @@ class ActionsPopup {
mcpLogPopup.close();
doctorPopup.close();
classpathPopup.close();
+ sendMessagePopup.close();
stopAllPopup.close();
captionOverlay.close();
}
@@ -236,7 +251,21 @@ class ActionsPopup {
return launchNotificationError;
}
+ void handlePaste(String text) {
+ if (sendMessagePopup.isVisible()) {
+ sendMessagePopup.handlePaste(text);
+ }
+ }
+
boolean handleKeyEvent(KeyEvent ke) {
+ if (sendMessagePopup.isVisible()) {
+ if (ke.isConfirm()) {
+ sendMessagePopup.doSend(ctx, scheduler);
+ } else {
+ sendMessagePopup.handleKeyEvent(ke);
+ }
+ return true;
+ }
if (mcpLogPopup.handleKeyEvent(ke)) {
return true;
}
@@ -355,6 +384,9 @@ class ActionsPopup {
} else if (action == ACTION_MCP_LOG) {
showActionsMenu = false;
openMcpLog();
+ } else if (action == ACTION_SEND_MESSAGE) {
+ showActionsMenu = false;
+ openSendMessage();
} else if (action == ACTION_RESET_STATS) {
showActionsMenu = false;
if (resetStatsAction != null) {
@@ -403,6 +435,9 @@ class ActionsPopup {
if (classpathPopup.isVisible()) {
classpathPopup.render(frame, area);
}
+ if (sendMessagePopup.isVisible()) {
+ sendMessagePopup.render(frame, area);
+ }
if (captionOverlay.isInlineMode()) {
captionOverlay.render(frame, area);
}
@@ -497,11 +532,12 @@ class ActionsPopup {
items.add(ListItem.from(" 📄 Tape Recording Guide"));
items.add(ListItem.from(" 🩺 Run Doctor"));
items.add(ListItem.from(" 📦 Show Classpath"));
- items.add(ListItem.from(" 🔄 Reset Stats"));
if (mcpEnabled) {
items.add(ListItem.from(" 🤖 MCP Info"));
items.add(ListItem.from(" 📋 MCP Log"));
}
+ items.add(ListItem.from(" 📩 Send Message"));
+ items.add(ListItem.from(" 🔄 Reset Stats"));
items.add(ListItem.from(stopLabel));
ListWidget list = ListWidget.builder()
.items(items.toArray(ListItem[]::new))
@@ -777,6 +813,40 @@ class ActionsPopup {
return index;
}
+ private void openSendMessage() {
+ if (ctx == null) {
+ return;
+ }
+ String pid = ctx.selectedPid;
+ if (pid == null) {
+ List<IntegrationInfo> ints = integrations.get();
+ List<IntegrationInfo> alive = ints.stream().filter(i ->
!i.vanishing && i.pid != null).toList();
+ if (alive.size() == 1) {
+ pid = alive.get(0).pid;
+ }
+ }
+ if (pid == null) {
+ setNotification("Select an integration first", true);
+ return;
+ }
+ IntegrationInfo info = findIntegration(pid);
+ if (info == null || info.routes.isEmpty()) {
+ setNotification("No routes available", true);
+ return;
+ }
+ sendMessagePopup.open(ctx, pid, info.name, info.routes,
preSelectedRouteId);
+ preSelectedRouteId = null;
+ }
+
+ private IntegrationInfo findIntegration(String pid) {
+ for (IntegrationInfo i : integrations.get()) {
+ if (pid.equals(i.pid)) {
+ return i;
+ }
+ }
+ return null;
+ }
+
private void openTapeInstructions() {
docLines = null;
docContent = "# Tape Recording Guide\n\n"
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index ba7e66f43ab0..9e5e8f376e0e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -64,6 +64,7 @@ import dev.tamboui.tui.event.Event;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.KeyModifiers;
+import dev.tamboui.tui.event.PasteEvent;
import dev.tamboui.tui.event.TickEvent;
import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.barchart.Bar;
@@ -343,6 +344,7 @@ public class CamelMonitor extends CamelCommand {
try (var tui = TuiBackendHelper.createTuiRunner()) {
this.runner = tui;
ctx.runner = tui;
+ actionsPopup.setScheduler(tui.scheduler());
// Intercept Ctrl+C: quit the TUI cleanly instead of letting
// the JVM tear down the classloader while we're still running
Signal.handle(new Signal("INT"), sig -> tui.quit());
@@ -511,6 +513,9 @@ public class CamelMonitor extends CamelCommand {
// F2 opens actions menu (global)
if (ke.isKey(KeyCode.F2)) {
+ if (tabsState.selected() == TAB_ROUTES && routesTab != null) {
+
actionsPopup.setPreSelectedRouteId(routesTab.selectedRouteId());
+ }
actionsPopup.open();
return true;
}
@@ -638,6 +643,12 @@ public class CamelMonitor extends CamelCommand {
return true;
}
}
+ if (event instanceof PasteEvent pe) {
+ if (actionsPopup.isVisible()) {
+ actionsPopup.handlePaste(pe.text());
+ return true;
+ }
+ }
if (event instanceof TickEvent) {
long now = System.currentTimeMillis();
boolean keyProcessed = false;
@@ -2188,8 +2199,12 @@ public class CamelMonitor extends CamelCommand {
refreshErrorData(pids);
}
- // Refresh trace data only when the Inspect tab is visible
+ // Refresh trace data only when the History tab is visible
if (tabsState.selected() == TAB_HISTORY) {
+ if (historyTab.historyRefreshRequested) {
+ historyTab.historyRefreshRequested = false;
+ refreshHistoryData(pids);
+ }
refreshTraceData(pids);
}
} catch (Exception e) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 48341aa1365b..8fbfae4162af 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -91,6 +91,7 @@ class HistoryTab implements MonitorTab {
private boolean historyWordWrap = true;
private int historyDetailScroll;
private int historyDetailHScroll;
+ volatile boolean historyRefreshRequested;
HistoryTab(MonitorContext ctx,
AtomicReference<List<TraceEntry>> traces,
@@ -226,6 +227,10 @@ class HistoryTab implements MonitorTab {
return true;
}
if (ke.isKey(KeyCode.F5)) {
+ historyEntries = Collections.emptyList();
+ historyDetailScroll = 0;
+ historyDetailHScroll = 0;
+ historyRefreshRequested = true;
return true;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 239239ba0337..f8ce5dd3cb6f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -843,6 +843,19 @@ class RoutesTab implements MonitorTab {
// ---- Route actions ----
+ String selectedRouteId() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.routes.isEmpty()) {
+ return null;
+ }
+ List<RouteInfo> sortedRoutes = new ArrayList<>(info.routes);
+ sortedRoutes.sort(this::sortRoute);
+ Integer sel = routeTableState.selected();
+ RouteInfo route = (sel != null && sel >= 0 && sel <
sortedRoutes.size())
+ ? sortedRoutes.get(sel) : sortedRoutes.get(0);
+ return route.routeId;
+ }
+
private String selectedRouteState() {
IntegrationInfo info = ctx.findSelectedIntegration();
if (info == null || info.routes.isEmpty()) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
new file mode 100644
index 000000000000..ae5b05614931
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java
@@ -0,0 +1,614 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+class SendMessagePopup {
+
+ private static final int FIELD_ROUTE = 0;
+ private static final int FIELD_BODY = 1;
+ private static final int FIELD_HEADERS = 2;
+ private static final int FIELD_MODE = 3;
+
+ private boolean visible;
+ private boolean sending;
+ private String pid;
+ private String integrationName;
+ private List<RouteInfo> routes;
+ private int selectedRouteIndex;
+ private final TextInputState bodyState = new TextInputState("");
+ private int selectedField = FIELD_BODY;
+ private boolean inOut;
+ private String resultMessage;
+ private boolean resultError;
+
+ private List<HeaderEntry> headers;
+ private int selectedHeader;
+ private boolean editingHeaderKey;
+
+ boolean isVisible() {
+ return visible;
+ }
+
+ void open(MonitorContext ctx, String pid, String name, List<RouteInfo>
routes, String preSelectRouteId) {
+ if (pid == null || routes == null || routes.isEmpty()) {
+ return;
+ }
+ this.pid = pid;
+ this.integrationName = name;
+ this.routes = new ArrayList<>(routes);
+ this.selectedRouteIndex = findSmartDefault(preSelectRouteId);
+ this.bodyState.clear();
+ this.selectedField = FIELD_BODY;
+ this.inOut = false;
+ this.resultMessage = null;
+ this.resultError = false;
+ this.sending = false;
+ this.headers = null;
+ this.selectedHeader = 0;
+ this.editingHeaderKey = true;
+ this.visible = true;
+ }
+
+ void close() {
+ visible = false;
+ }
+
+ boolean handleKeyEvent(KeyEvent ke) {
+ if (!visible) {
+ return false;
+ }
+ if (sending) {
+ return true;
+ }
+ if (ke.isCancel()) {
+ close();
+ return true;
+ }
+ if (ke.isConfirm()) {
+ return true;
+ }
+ if (selectedField == FIELD_BODY) {
+ if (ke.isKey(KeyCode.TAB) || ke.isDown()) {
+ if (hasHeaders()) {
+ selectedField = FIELD_HEADERS;
+ selectedHeader = 0;
+ editingHeaderKey = true;
+ } else {
+ selectedField = FIELD_MODE;
+ }
+ return true;
+ }
+ if (ke.isUp()) {
+ if (routes.size() > 1) {
+ selectedField = FIELD_ROUTE;
+ }
+ return true;
+ }
+ handleTextInput(ke, bodyState);
+ return true;
+ }
+ if (selectedField == FIELD_ROUTE) {
+ if (ke.isKey(KeyCode.TAB) || ke.isDown()) {
+ selectedField = FIELD_BODY;
+ return true;
+ }
+ if (ke.isUp()) {
+ selectedField = FIELD_MODE;
+ return true;
+ }
+ if (ke.isLeft()) {
+ selectedRouteIndex = (selectedRouteIndex - 1 + routes.size())
% routes.size();
+ return true;
+ }
+ if (ke.isRight()) {
+ selectedRouteIndex = (selectedRouteIndex + 1) % routes.size();
+ return true;
+ }
+ return true;
+ }
+ if (selectedField == FIELD_HEADERS) {
+ return handleHeaderKeyEvent(ke);
+ }
+ if (selectedField == FIELD_MODE) {
+ if (ke.isKey(KeyCode.TAB) || ke.isDown()) {
+ if (routes.size() > 1) {
+ selectedField = FIELD_ROUTE;
+ } else {
+ selectedField = FIELD_BODY;
+ }
+ return true;
+ }
+ if (ke.isUp()) {
+ if (hasHeaders()) {
+ selectedField = FIELD_HEADERS;
+ selectedHeader = headers.size() - 1;
+ editingHeaderKey = false;
+ } else {
+ selectedField = FIELD_BODY;
+ }
+ return true;
+ }
+ if (ke.isChar('+')) {
+ addHeader();
+ return true;
+ }
+ if (ke.isLeft() || ke.isRight() || ke.code() == KeyCode.CHAR) {
+ inOut = !inOut;
+ return true;
+ }
+ return true;
+ }
+ return true;
+ }
+
+ private boolean handleHeaderKeyEvent(KeyEvent ke) {
+ HeaderEntry current = headers.get(selectedHeader);
+ TextInputState activeInput = editingHeaderKey ? current.keyInput :
current.valueInput;
+
+ if (ke.isChar('+')) {
+ addHeader();
+ return true;
+ }
+ if (ke.isKey(KeyCode.TAB) || ke.isDown()) {
+ if (editingHeaderKey) {
+ editingHeaderKey = false;
+ } else if (selectedHeader < headers.size() - 1) {
+ selectedHeader++;
+ editingHeaderKey = true;
+ } else {
+ selectedField = FIELD_MODE;
+ }
+ return true;
+ }
+ if (ke.isUp()) {
+ if (editingHeaderKey) {
+ if (selectedHeader > 0) {
+ selectedHeader--;
+ editingHeaderKey = false;
+ } else {
+ selectedField = FIELD_BODY;
+ }
+ } else {
+ editingHeaderKey = true;
+ }
+ return true;
+ }
+ if (ke.isDeleteBackward()) {
+ if (editingHeaderKey && current.keyInput.text().isEmpty()) {
+ headers.remove(selectedHeader);
+ if (headers.isEmpty()) {
+ headers = null;
+ selectedField = FIELD_BODY;
+ } else if (selectedHeader >= headers.size()) {
+ selectedHeader = headers.size() - 1;
+ }
+ return true;
+ }
+ activeInput.deleteBackward();
+ return true;
+ }
+ if (ke.isDeleteForward()) {
+ activeInput.deleteForward();
+ return true;
+ }
+ if (ke.isLeft()) {
+ if (!editingHeaderKey && activeInput.cursorPosition() == 0) {
+ editingHeaderKey = true;
+ } else {
+ activeInput.moveCursorLeft();
+ }
+ return true;
+ }
+ if (ke.isRight()) {
+ if (editingHeaderKey && activeInput.cursorPosition() ==
activeInput.text().length()) {
+ editingHeaderKey = false;
+ } else {
+ activeInput.moveCursorRight();
+ }
+ return true;
+ }
+ if (ke.isHome()) {
+ activeInput.moveCursorToStart();
+ return true;
+ }
+ if (ke.isEnd()) {
+ activeInput.moveCursorToEnd();
+ return true;
+ }
+ if (ke.code() == KeyCode.CHAR) {
+ activeInput.insert(ke.character());
+ return true;
+ }
+ return true;
+ }
+
+ private void addHeader() {
+ if (headers == null) {
+ headers = new ArrayList<>();
+ }
+ headers.add(new HeaderEntry(new TextInputState(""), new
TextInputState("")));
+ selectedField = FIELD_HEADERS;
+ selectedHeader = headers.size() - 1;
+ editingHeaderKey = true;
+ }
+
+ private boolean hasHeaders() {
+ return headers != null && !headers.isEmpty();
+ }
+
+ void handlePaste(String text) {
+ if (!visible || sending || text == null || text.isEmpty()) {
+ return;
+ }
+ TextInputState target = activeTextInput();
+ if (target != null) {
+ for (int i = 0; i < text.length(); i++) {
+ char ch = text.charAt(i);
+ if (ch != '\n' && ch != '\r') {
+ target.insert(ch);
+ }
+ }
+ }
+ }
+
+ private TextInputState activeTextInput() {
+ if (selectedField == FIELD_BODY) {
+ return bodyState;
+ }
+ if (selectedField == FIELD_HEADERS && hasHeaders()) {
+ HeaderEntry he = headers.get(selectedHeader);
+ return editingHeaderKey ? he.keyInput : he.valueInput;
+ }
+ return null;
+ }
+
+ void doSend(MonitorContext ctx, ScheduledExecutorService scheduler) {
+ if (!visible || sending || ctx == null || scheduler == null) {
+ return;
+ }
+ sending = true;
+ resultMessage = "Sending...";
+ resultError = false;
+
+ String body = bodyState.text();
+ RouteInfo route = routes.get(selectedRouteIndex);
+ String endpoint = route.routeId;
+ String mep = inOut ? "InOut" : "InOnly";
+ String targetPid = pid;
+ List<HeaderEntry> hdrs = headers != null ? new ArrayList<>(headers) :
null;
+
+ scheduler.execute(() -> {
+ try {
+ Path outputFile = ctx.getOutputFile(targetPid);
+ PathUtils.deleteFile(outputFile);
+
+ JsonObject root = new JsonObject();
+ root.put("action", "send");
+ root.put("endpoint", endpoint);
+ root.put("body", body);
+ root.put("exchangePattern", mep);
+ root.put("pollTimeout", 20000);
+ root.put("poll", false);
+
+ if (hdrs != null && !hdrs.isEmpty()) {
+ JsonArray arr = new JsonArray();
+ for (HeaderEntry he : hdrs) {
+ String k = he.keyInput.text().trim();
+ String v = he.valueInput.text();
+ if (!k.isEmpty()) {
+ JsonObject jo = new JsonObject();
+ jo.put("key", k);
+ jo.put("value", v);
+ arr.add(jo);
+ }
+ }
+ if (!arr.isEmpty()) {
+ root.put("headers", arr);
+ }
+ }
+
+ Path actionFile = ctx.getActionFile(targetPid);
+ PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+ JsonObject response =
MonitorContext.pollJsonResponse(outputFile, 25000);
+ PathUtils.deleteFile(outputFile);
+
+ if (response != null) {
+ String status = response.getString("status");
+ Object elapsed = response.get("elapsed");
+ if ("success".equals(status)) {
+ String msg = "Sent (" + elapsed + "ms)";
+ JsonObject message = response.getMap("message");
+ if (inOut && message != null) {
+ String replyBody =
objToString(message.get("body"));
+ if (replyBody != null && !replyBody.isEmpty()) {
+ msg += " - Reply: " + truncate(replyBody, 40);
+ }
+ }
+ resultMessage = msg;
+ resultError = false;
+ } else {
+ JsonObject exception = response.getMap("exception");
+ if (exception != null) {
+ String exMsg = exception.getString("message");
+ resultMessage = "Error: " + (exMsg != null ?
truncate(exMsg, 50) : status);
+ } else {
+ resultMessage = "Error: " + (status != null ?
status : "unknown");
+ }
+ resultError = true;
+ }
+ } else {
+ resultMessage = "No response from integration";
+ resultError = true;
+ }
+ } catch (Exception e) {
+ resultMessage = "Error: " + e.getMessage();
+ resultError = true;
+ } finally {
+ sending = false;
+ }
+ });
+ }
+
+ void render(Frame frame, Rect area) {
+ if (!visible) {
+ return;
+ }
+
+ int headerCount = hasHeaders() ? headers.size() : 0;
+ int popupW = Math.min(80, area.width() - 4);
+ int baseH = routes.size() > 1 ? 14 : 12;
+ int popupH = baseH + (headerCount > 0 ? headerCount + 1 : 0);
+ int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+ int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+ Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+
+ String title = " Send Message";
+ if (integrationName != null) {
+ title += " - " + truncate(integrationName, 20);
+ }
+ title += " ";
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED)
+ .title(title)
+ .titleBottom(Title.from(Line.from(
+ Span.styled(" +", MonitorContext.HINT_KEY_STYLE),
+ Span.raw(" header │"),
+ Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE),
+ Span.raw(" send │"),
+ Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE),
+ Span.raw(" close "))))
+ .build();
+ frame.renderWidget(block, popup);
+
+ int innerX = popup.left() + 2;
+ int innerW = popup.width() - 4;
+ int labelW = 8;
+ int fieldW = innerW - labelW;
+ int row = popup.top() + 1;
+
+ // Route selector (only if multiple routes)
+ if (routes.size() > 1) {
+ row++;
+ renderLabel(frame, innerX, row, labelW, "Route:", selectedField ==
FIELD_ROUTE);
+ RouteInfo ri = routes.get(selectedRouteIndex);
+ String routeDisplay = ri.routeId + " (" + truncateUri(ri.from,
fieldW - ri.routeId.length() - 6) + ")";
+ String arrow = selectedField == FIELD_ROUTE ? "â—€ " : " ";
+ String arrowR = selectedField == FIELD_ROUTE ? " â–¶" : " ";
+ Style routeStyle = selectedField == FIELD_ROUTE ?
Style.EMPTY.bold() : Style.EMPTY;
+ Rect routeArea = new Rect(innerX + labelW, row, fieldW, 1);
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(arrow, routeStyle),
+ Span.styled(routeDisplay, routeStyle),
+ Span.styled(arrowR, routeStyle))), routeArea);
+ }
+
+ // Body input
+ row += 2;
+ renderLabel(frame, innerX, row, labelW, "Body:", selectedField ==
FIELD_BODY);
+ Rect bodyArea = new Rect(innerX + labelW, row, fieldW, 1);
+ if (selectedField == FIELD_BODY && !sending) {
+ TextInput textInput = TextInput.builder()
+ .cursorStyle(Style.EMPTY.reversed())
+ .build();
+ frame.renderStatefulWidget(textInput, bodyArea, bodyState);
+ } else {
+ String text = bodyState.text();
+ Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(text.isEmpty() ? "—" : text, style))),
bodyArea);
+ }
+
+ // Headers section
+ if (hasHeaders()) {
+ row++;
+ int keyW = Math.min(20, fieldW / 3);
+ int valW = fieldW - keyW - 3;
+ for (int i = 0; i < headers.size(); i++) {
+ row++;
+ boolean isSelected = selectedField == FIELD_HEADERS &&
selectedHeader == i;
+ String label = i == 0 ? "Hdrs:" : "";
+ renderLabel(frame, innerX, row, labelW, label,
+ isSelected || (i == 0 && selectedField ==
FIELD_HEADERS));
+
+ HeaderEntry he = headers.get(i);
+ int fieldX = innerX + labelW;
+
+ // Key field
+ Rect keyArea = new Rect(fieldX, row, keyW, 1);
+ if (isSelected && editingHeaderKey && !sending) {
+ TextInput keyInput = TextInput.builder()
+ .cursorStyle(Style.EMPTY.reversed())
+ .build();
+ frame.renderStatefulWidget(keyInput, keyArea, he.keyInput);
+ } else {
+ String keyText = he.keyInput.text();
+ Style keyStyle;
+ if (keyText.isEmpty()) {
+ keyStyle = Style.EMPTY.dim();
+ keyText = "<key>";
+ } else {
+ keyStyle = isSelected ? Style.EMPTY.bold() :
Style.EMPTY;
+ }
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(keyText, keyStyle))), keyArea);
+ }
+
+ // Separator
+ Rect sepArea = new Rect(fieldX + keyW, row, 3, 1);
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(" : ", Style.EMPTY.dim()))), sepArea);
+
+ // Value field
+ Rect valArea = new Rect(fieldX + keyW + 3, row, valW, 1);
+ if (isSelected && !editingHeaderKey && !sending) {
+ TextInput valInput = TextInput.builder()
+ .cursorStyle(Style.EMPTY.reversed())
+ .build();
+ frame.renderStatefulWidget(valInput, valArea,
he.valueInput);
+ } else {
+ String valText = he.valueInput.text();
+ Style valStyle;
+ if (valText.isEmpty()) {
+ valStyle = Style.EMPTY.dim();
+ valText = "<value>";
+ } else {
+ valStyle = isSelected ? Style.EMPTY.bold() :
Style.EMPTY;
+ }
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(valText, valStyle))), valArea);
+ }
+ }
+ }
+
+ // Mode toggle
+ row += 2;
+ renderLabel(frame, innerX, row, labelW, "Mode:", selectedField ==
FIELD_MODE);
+ Rect modeArea = new Rect(innerX + labelW, row, fieldW, 1);
+ Style inOnlyStyle = !inOut ? Style.EMPTY.bold().reversed() :
Style.EMPTY.dim();
+ Style inOutStyle = inOut ? Style.EMPTY.bold().reversed() :
Style.EMPTY.dim();
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(" InOnly ", inOnlyStyle),
+ Span.raw(" "),
+ Span.styled(" InOut ", inOutStyle))), modeArea);
+
+ // Result line
+ if (resultMessage != null) {
+ row += 2;
+ Style resultStyle = resultError
+ ? Style.EMPTY.fg(Color.LIGHT_RED)
+ : Style.EMPTY.fg(Color.GREEN);
+ Rect resultArea = new Rect(innerX, row, innerW, 1);
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(resultMessage, resultStyle))), resultArea);
+ }
+ }
+
+ private int findSmartDefault(String preSelectRouteId) {
+ if (preSelectRouteId != null) {
+ for (int i = 0; i < routes.size(); i++) {
+ if (preSelectRouteId.equals(routes.get(i).routeId)) {
+ return i;
+ }
+ }
+ }
+ for (int i = 0; i < routes.size(); i++) {
+ String from = routes.get(i).from;
+ if (from != null && (from.startsWith("direct:") ||
from.startsWith("seda:")
+ || from.startsWith("platform-http:"))) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ private void handleTextInput(KeyEvent ke, TextInputState state) {
+ if (ke.isDeleteBackward()) {
+ state.deleteBackward();
+ } else if (ke.isDeleteForward()) {
+ state.deleteForward();
+ } else if (ke.isLeft()) {
+ state.moveCursorLeft();
+ } else if (ke.isRight()) {
+ state.moveCursorRight();
+ } else if (ke.isHome()) {
+ state.moveCursorToStart();
+ } else if (ke.isEnd()) {
+ state.moveCursorToEnd();
+ } else if (ke.code() == KeyCode.CHAR) {
+ state.insert(ke.character());
+ }
+ }
+
+ private void renderLabel(Frame frame, int x, int y, int w, String label,
boolean selected) {
+ Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim();
+ Rect labelArea = new Rect(x, y, w, 1);
+ frame.renderWidget(Paragraph.from(Line.from(Span.styled(label,
style))), labelArea);
+ }
+
+ private static String truncate(String s, int max) {
+ if (s == null) {
+ return "";
+ }
+ return s.length() <= max ? s : s.substring(0, max - 1) + "…";
+ }
+
+ private static String truncateUri(String uri, int max) {
+ if (uri == null) {
+ return "";
+ }
+ int q = uri.indexOf('?');
+ String clean = q > 0 ? uri.substring(0, q) : uri;
+ return truncate(clean, max);
+ }
+
+ private static String objToString(Object obj) {
+ if (obj == null) {
+ return null;
+ }
+ return obj.toString();
+ }
+
+ record HeaderEntry(TextInputState keyInput, TextInputState valueInput) {
+ }
+}