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 8e517ccdcf09 camel-jbang - TUI: free Ctrl+R for history search, move
tape recording to F2 only
8e517ccdcf09 is described below
commit 8e517ccdcf0923db2f96504e519992597b766afe
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 25 18:38:22 2026 +0200
camel-jbang - TUI: free Ctrl+R for history search, move tape recording to
F2 only
Remove the Ctrl+R keyboard shortcut for tape recording toggle so it can
be used for history search. Tape recording is now only accessible via
the F2 actions menu. When stopping a tape recording via F2, the menu
interaction (F2 key, navigation, Enter) is rolled back from the tape to
keep it clean.
camel-jbang - TUI: add /clear-history slash command to AI prompt panel
Add /clear-history command to the F8 AI prompt panel that clears the
persisted prompt history (navigated with Up/Down arrows). Clears both
the in-memory entries and the history file on disk.
camel-jbang - TUI: implement Ctrl+R reverse-i-search in AI prompt panel
Add JLine-style reverse incremental history search to the F8 AI prompt
panel. Press Ctrl+R to enter search mode, type to narrow matches
(case-insensitive substring), Ctrl+R again to cycle to older matches,
Enter to accept, Escape to cancel. The search prompt shows
"bck-i-search: query" or "failing bck-i-search: query" with a preview
of the matched entry.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 4 +-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 126 +++++++++++++++++++++
.../core/commands/tui/AiSlashCommandContext.java | 2 +
.../core/commands/tui/AiSlashCommandRegistry.java | 6 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 17 ++-
.../jbang/core/commands/tui/DocViewerPopup.java | 5 +-
.../jbang/core/commands/tui/RecordingManager.java | 19 ++++
.../dsl/jbang/core/commands/tui/TapeRecorder.java | 13 +++
.../jbang/core/commands/tui/TuiPromptHistory.java | 28 +++++
.../dsl/jbang/core/commands/tui/AiPanelTest.java | 20 +++-
.../commands/tui/AiSlashCommandRegistryTest.java | 8 +-
11 files changed, 235 insertions(+), 13 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 dc4a1e8d4c79..65905a55fbf6 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
@@ -1029,8 +1029,8 @@ class ActionsPopup {
? TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Hide Keystrokes")
: TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Show Keystrokes");
String tapeLabel = tapeRecordingActive.get()
- ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording
(Ctrl+R)")
- : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording
(Ctrl+R)");
+ ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording")
+ : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording");
List<ListItem> items = new ArrayList<>();
items.add(ListItem.from(" .."));
items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 9d2e93b10451..ed6f0daee7ac 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
@@ -100,6 +100,12 @@ class AiPanel {
private int cursorPos;
private TuiPromptHistory promptHistory;
+ // Reverse-i-search state (Ctrl+R)
+ private boolean historySearchActive;
+ private final StringBuilder searchTerm = new StringBuilder();
+ private int searchIndex = -1;
+ private String savedInput;
+
// TAB completion cycle state. completionMatches holds the candidate
command names for the active cycle;
// completionSnapshot is the buffer text a TAB press last produced. The
cycle continues only while the buffer still
// equals that snapshot, so any other edit (typing, backspace, cursor
move) transparently starts a fresh completion.
@@ -374,6 +380,9 @@ class AiPanel {
}
return true;
}
+ if (historySearchActive) {
+ return handleSearchKeyEvent(ke);
+ }
if (ke.isKey(KeyCode.F8)) {
close();
return true;
@@ -381,6 +390,11 @@ class AiPanel {
if (isFunctionKey(ke)) {
return false;
}
+ if (!statsView && !thinking.get() && ke.hasCtrl() &&
ke.isCharIgnoreCase('r')
+ && promptHistory != null && promptHistory.isEnabled() &&
promptHistory.size() > 0) {
+ enterHistorySearch();
+ return true;
+ }
if (ke.hasCtrl() && ke.isCharIgnoreCase('p') && !thinking.get() &&
activeCliCommand == null) {
openProviderSwitch();
return true;
@@ -495,6 +509,78 @@ class AiPanel {
return true;
}
+ // ---- Reverse-i-search (Ctrl+R) ----
+
+ private void enterHistorySearch() {
+ savedInput = inputBuffer.toString();
+ historySearchActive = true;
+ searchTerm.setLength(0);
+ searchIndex = -1;
+ }
+
+ private void exitHistorySearch(boolean accept) {
+ historySearchActive = false;
+ if (accept && searchIndex >= 0) {
+ replaceInputBuffer(promptHistory.get(searchIndex));
+ } else {
+ replaceInputBuffer(savedInput);
+ }
+ searchTerm.setLength(0);
+ searchIndex = -1;
+ savedInput = null;
+ }
+
+ private void performSearch(int fromIndex) {
+ if (searchTerm.isEmpty()) {
+ searchIndex = -1;
+ return;
+ }
+ searchIndex = promptHistory.searchBackward(searchTerm.toString(),
fromIndex);
+ }
+
+ private boolean handleSearchKeyEvent(KeyEvent ke) {
+ if (ke.hasCtrl() && ke.isCharIgnoreCase('r')) {
+ if (searchIndex > 0) {
+ performSearch(searchIndex - 1);
+ }
+ return true;
+ }
+ if (ke.isKey(KeyCode.ENTER)) {
+ exitHistorySearch(true);
+ return true;
+ }
+ if (ke.isKey(KeyCode.ESCAPE) || ke.isCtrlC()) {
+ exitHistorySearch(false);
+ return true;
+ }
+ if (ke.isKey(KeyCode.BACKSPACE)) {
+ if (searchTerm.isEmpty()) {
+ exitHistorySearch(false);
+ } else {
+ searchTerm.deleteCharAt(searchTerm.length() - 1);
+ performSearch(promptHistory.size() - 1);
+ }
+ return true;
+ }
+ if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
+ searchTerm.append(ke.character());
+ int from = searchIndex >= 0 ? searchIndex : promptHistory.size() -
1;
+ performSearch(from);
+ return true;
+ }
+ // Any other key: accept match and let the key be processed normally
+ exitHistorySearch(true);
+ return handleKeyEvent(ke);
+ }
+
+ boolean isHistorySearchActive() {
+ return historySearchActive;
+ }
+
+ String searchTermForTesting() {
+ return searchTerm.toString();
+ }
+
/**
* 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
@@ -1060,6 +1146,11 @@ class AiPanel {
}
private void renderInput(Frame frame, Rect area) {
+ if (historySearchActive) {
+ renderSearchInput(frame, area);
+ return;
+ }
+
String prompt = INPUT_PROMPT;
String text = inputBuffer.toString();
@@ -1105,6 +1196,33 @@ class AiPanel {
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
}
+ private void renderSearchInput(Frame frame, Rect area) {
+ boolean failing = !searchTerm.isEmpty() && searchIndex < 0;
+ String label = failing ? "failing bck-i-search: " : "bck-i-search: ";
+ String query = searchTerm.toString();
+
+ List<Span> spans = new ArrayList<>();
+ Style labelStyle = failing
+ ? Theme.error()
+ : Style.EMPTY.fg(Theme.accent());
+ spans.add(Span.styled(label, labelStyle));
+ spans.add(Span.raw(query));
+ spans.add(Span.styled("_", Style.EMPTY.reversed()));
+
+ if (searchIndex >= 0) {
+ String matched = promptHistory.get(searchIndex);
+ int maxWidth = area.width() - label.length() - query.length() - 1;
+ if (maxWidth > 4) {
+ String preview = matched.length() > maxWidth
+ ? matched.substring(0, maxWidth - 1) + "…"
+ : matched;
+ spans.add(Span.styled(" " + preview, Style.EMPTY.dim()));
+ }
+ }
+
+ frame.renderWidget(Paragraph.from(Line.from(spans)), area);
+ }
+
void renderFooter(List<Span> spans) {
TuiHelper.hint(spans, "F8", "close");
if (statsView) {
@@ -1120,6 +1238,7 @@ class AiPanel {
TuiHelper.hint(spans, "Ctrl+P", "provider");
if (!thinking.get()) {
TuiHelper.hint(spans, "Enter", "send");
+ TuiHelper.hint(spans, "Ctrl+R", "search");
} else {
TuiHelper.hint(spans, "Esc/Ctrl+C", "interrupt");
}
@@ -1602,6 +1721,13 @@ class AiPanel {
AiPanel.this.clearConversation();
}
+ @Override
+ public void clearHistory() {
+ if (promptHistory != null) {
+ promptHistory.clear();
+ }
+ }
+
@Override
public String currentModel() {
return client != null && client.model() != null ? client.model() :
"unknown";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
index ceac4afd2ec3..98e6b7adee33 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandContext.java
@@ -28,6 +28,8 @@ interface AiSlashCommandContext {
void clearConversation();
+ void clearHistory();
+
String currentModel();
List<String> availableModels();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
index d755b6c689c3..12b1a3f6481d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistry.java
@@ -74,6 +74,12 @@ final class AiSlashCommandRegistry {
context.clearConversation();
return CommandResult.system("");
}));
+ commands.add(new Descriptor(
+ "clear-history", List.of(), "Clear the prompt history", null,
+ (context, arguments) -> {
+ context.clearHistory();
+ return CommandResult.system("Prompt history cleared");
+ }));
commands.add(new Descriptor(
"close", List.of(), "Close the AI panel", null,
(context, arguments) -> {
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 7d4477a05f3a..403d094ff55b 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
@@ -588,7 +588,9 @@ public class CamelMonitor extends CamelCommand {
if (event instanceof KeyEvent ke) {
recordingManager.recordKey(ke, mcpInjectedKey);
if (ke.hasCtrl() && ke.isChar('r')) {
- recordingManager.toggleTapeRecording();
+ if (aiPanel.isOpen()) {
+ return aiPanel.handleKeyEvent(ke);
+ }
return true;
}
if (captionOverlay.isVisible()) {
@@ -621,7 +623,11 @@ public class CamelMonitor extends CamelCommand {
return shellPanel.handleKeyEvent(ke);
}
if (actionsPopup.isVisible()) {
- return actionsPopup.handleKeyEvent(ke);
+ boolean result = actionsPopup.handleKeyEvent(ke);
+ if (!actionsPopup.isVisible()) {
+ recordingManager.rollbackMenu();
+ }
+ return result;
}
if (popupManager.handleKeyEvent(ke,
tabRegistry.selectedTabIndex(), TAB_LOG)) {
return true;
@@ -831,6 +837,7 @@ public class CamelMonitor extends CamelCommand {
return true;
}
if (ke.isKey(KeyCode.F2)) {
+ recordingManager.markMenuOpened();
if (tabRegistry.selectedTabIndex() == TAB_ROUTES &&
tabRegistry.routesTab() != null) {
actionsPopup.setPreSelectedRouteId(tabRegistry.routesTab().selectedRouteId());
}
@@ -948,7 +955,11 @@ public class CamelMonitor extends CamelCommand {
// Modal popups capture all mouse events (including tab bar and footer)
if (actionsPopup.isVisible()) {
- return actionsPopup.handleMouseEvent(me);
+ boolean result = actionsPopup.handleMouseEvent(me);
+ if (!actionsPopup.isVisible()) {
+ recordingManager.rollbackMenu();
+ }
+ return result;
}
// Tab bar clicks: detect which tab was clicked and switch to it
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 c1b472d2c9c4..ee753c3ac59d 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
@@ -361,8 +361,7 @@ class DocViewerPopup {
+ "with timing. The tape can be
replayed inside the TUI to produce\n"
+ "an Asciinema `.cast`
recording.\n\n"
+ "## Starting and Stopping\n\n"
- + "- Press **Ctrl+R** to
start/stop recording at any time\n"
- + "- Or use the **F2** actions
menu → Start/Stop Tape Recording\n\n"
+ + "- Use the **F2** actions menu
→ Start/Stop Tape Recording\n\n"
+ "When recording stops, the tape
is saved to the current directory as\n"
+
"`camel-tui-tape-<timestamp>.tape`.\n\n"
+ "## Replaying a Tape\n\n"
@@ -376,7 +375,7 @@ class DocViewerPopup {
+ " agg recording.cast
demo.gif\n\n"
+ "Or upload to
[asciinema.org](https://asciinema.org) for a shareable link.\n\n"
+ "## Tips\n\n"
- + "- **Ctrl+R** is not captured
in the tape, keeping the script clean\n"
+ + "- Tape recording toggle is not
captured in the tape, keeping the script clean\n"
+ "- Natural pauses between
keystrokes are preserved as `Sleep` commands\n"
+ "- Keep recordings focused —
one workflow at a time works best\n");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
index e6c2ad205ffd..86f62ebd3096 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
@@ -50,6 +50,7 @@ class RecordingManager {
private volatile boolean pendingScreenshot;
private final List<KeyRecord> recentKeys = new ArrayList<>();
private TuiEventLog eventLog;
+ private int menuTapePosition = -1;
RecordingManager(CaptionOverlay captionOverlay) {
this.captionOverlay = captionOverlay;
@@ -151,6 +152,23 @@ class RecordingManager {
return null;
}
+ // ---- Tape menu rollback ----
+
+ void markMenuOpened() {
+ TapeRecorder tr = tapeRecorderRef.get();
+ if (tr != null && tr.isActive()) {
+ menuTapePosition = tr.preKeyPosition();
+ }
+ }
+
+ void rollbackMenu() {
+ TapeRecorder tr = tapeRecorderRef.get();
+ if (tr != null && tr.isActive() && menuTapePosition >= 0) {
+ tr.trimTo(menuTapePosition);
+ }
+ menuTapePosition = -1;
+ }
+
// ---- Tape recording ----
AtomicReference<TapeRecorder> tapeRecorderRef() {
@@ -179,6 +197,7 @@ class RecordingManager {
void toggleTapeRecording() {
TapeRecorder rec = tapeRecorderRef.get();
if (rec != null && rec.isActive()) {
+ rollbackMenu();
String tape = rec.stop();
tapeRecorderRef.set(null);
String timestamp = LocalDateTime.now()
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
index 15da52713f77..503965519fd9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
@@ -33,6 +33,7 @@ class TapeRecorder {
private long lastEventTime;
private int keyCount;
private boolean active;
+ private int preKeyPosition = -1;
void start(String title) {
lines.clear();
@@ -56,6 +57,7 @@ class TapeRecorder {
if (!active || key == null) {
return;
}
+ preKeyPosition = lines.size();
long now = System.currentTimeMillis();
long elapsed = now - lastEventTime;
if (elapsed > 200) {
@@ -71,6 +73,17 @@ class TapeRecorder {
lastEventTime = now;
}
+ int preKeyPosition() {
+ return preKeyPosition;
+ }
+
+ void trimTo(int position) {
+ if (position >= 0 && position < lines.size()) {
+ lines.subList(position, lines.size()).clear();
+ lastEventTime = System.currentTimeMillis();
+ }
+ }
+
void recordKeys(List<String> keys, int delay) {
if (!active) {
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPromptHistory.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPromptHistory.java
index 8be1dd760283..45df46d62e9d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPromptHistory.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPromptHistory.java
@@ -112,6 +112,34 @@ final class TuiPromptHistory {
return Optional.of(entries.get(browseIndex));
}
+ int size() {
+ return entries.size();
+ }
+
+ String get(int index) {
+ return entries.get(index);
+ }
+
+ int searchBackward(String query, int fromIndex) {
+ if (!isEnabled() || query == null || query.isEmpty() ||
entries.isEmpty()) {
+ return -1;
+ }
+ String lower = query.toLowerCase(java.util.Locale.ROOT);
+ int start = Math.min(fromIndex, entries.size() - 1);
+ for (int i = start; i >= 0; i--) {
+ if
(entries.get(i).toLowerCase(java.util.Locale.ROOT).contains(lower)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ void clear() {
+ entries.clear();
+ resetNavigation();
+ persist();
+ }
+
void resetNavigation() {
browseIndex = -1;
draft = "";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index 850b4515a7d7..13f9a9189c50 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -663,7 +663,7 @@ class AiPanelTest {
panel.open();
type(panel, "/c");
- // /c matches both /clear and /close, so the first TAB fills in their
common prefix without choosing one.
+ // /c matches /clear, /clear-history, and /close, so the first TAB
fills in their common prefix.
tab(panel);
assertEquals("/cl", panel.inputBufferForTesting());
@@ -671,6 +671,8 @@ class AiPanelTest {
tab(panel);
assertEquals("/clear", panel.inputBufferForTesting());
tab(panel);
+ assertEquals("/clear-history", panel.inputBufferForTesting());
+ tab(panel);
assertEquals("/close", panel.inputBufferForTesting());
tab(panel);
assertEquals("/clear", panel.inputBufferForTesting());
@@ -688,6 +690,8 @@ class AiPanelTest {
shiftTab(panel);
assertEquals("/close", panel.inputBufferForTesting());
shiftTab(panel);
+ assertEquals("/clear-history", panel.inputBufferForTesting());
+ shiftTab(panel);
assertEquals("/clear", panel.inputBufferForTesting());
}
@@ -711,11 +715,17 @@ class AiPanelTest {
tab(panel);
assertEquals("/clear", panel.inputBufferForTesting());
- // Backspacing breaks the cycle; the next TAB recomputes from the
edited buffer instead of jumping to /close.
+ // Backspacing breaks the cycle; the next TAB recomputes from the
edited buffer.
+ // /clea matches /clear and /clear-history, so TAB fills common prefix
/clear.
panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.BACKSPACE,
KeyModifiers.NONE));
assertEquals("/clea", panel.inputBufferForTesting());
tab(panel);
- assertEquals("/clear ", panel.inputBufferForTesting());
+ assertEquals("/clear", panel.inputBufferForTesting());
+ // First cycle step lands on /clear (index 0, same as prefix), next
advances to /clear-history.
+ tab(panel);
+ assertEquals("/clear", panel.inputBufferForTesting());
+ tab(panel);
+ assertEquals("/clear-history", panel.inputBufferForTesting());
}
private static void type(AiPanel panel, String text) {
@@ -825,6 +835,10 @@ class AiPanelTest {
public void clearConversation() {
}
+ @Override
+ public void clearHistory() {
+ }
+
@Override
public String currentModel() {
return "test-model";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
index 9c4be2a4e613..37f18f37375f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiSlashCommandRegistryTest.java
@@ -33,7 +33,7 @@ class AiSlashCommandRegistryTest {
void descriptorsKeepStableOrder() {
AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
- assertEquals(List.of("help", "provider", "model", "clear", "close",
"quit", "run", "infra", "send"),
+ assertEquals(List.of("help", "provider", "model", "clear",
"clear-history", "close", "quit", "run", "infra", "send"),
registry.descriptors().stream().map(AiSlashCommandRegistry.Descriptor::name).toList());
}
@@ -73,7 +73,7 @@ class AiSlashCommandRegistryTest {
void completionsIncludeAllCommandsForBareSlash() {
AiSlashCommandRegistry registry = AiSlashCommandRegistry.defaults();
- assertEquals(9, registry.completionsFor("/").size());
+ assertEquals(10, registry.completionsFor("/").size());
assertFalse(registry.completionsFor("/").stream().anyMatch(descriptor
-> "exit".equals(descriptor.name())));
}
@@ -290,6 +290,10 @@ class AiSlashCommandRegistryTest {
public void clearConversation() {
}
+ @Override
+ public void clearHistory() {
+ }
+
@Override
public String currentModel() {
return "test-model";